diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..b2c132b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: I want to fix a bug, but need some help +about: > + If the bug is easy to reproduce, we will help. However, you must fix the bug, + in a reasonable amount of time, or your issue will be closed. See + CONTRIBUTING.md + +--- + +ISSUES THAT DO NOT FOLLOW THIS TEMPLATE WILL BE CLOSED IMMEDIATELY. + +- [ ] This is not a usage question. + - Our volunteers' time is limited, so please ask usage questions on + [StackOverflow](http://stackoverflow.com/questions/tagged/authlogic). +- [ ] This is not a security issue. + - Do not disclose security issues in public. See our [contributing + guide](https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md) + for instructions. +- [ ] This bug is reproducible with a clean install of authlogic +- [ ] I am committed to fixing this in a reasonable amount of time, and + responding promptly to feedback. + +# Expected Behavior + +Describe. + +# Actual Behavior + +Describe. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..568db11a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: I have a usage question + url: http://stackoverflow.com/questions/tagged/authlogic + about: Due to limited volunteers, we cannot answer usage questions on GitHub. + - name: I found a security vulnerability + url: https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md + about: Please email the maintainers as described in CONTRIBUTING.md diff --git a/.github/ISSUE_TEMPLATE/feature_proposal.md b/.github/ISSUE_TEMPLATE/feature_proposal.md new file mode 100644 index 00000000..99de5f79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_proposal.md @@ -0,0 +1,33 @@ +--- +name: Feature Proposal +about: > + Propose something that you would like to build. We'll help, but you must build + it yourself, in a reasonable amount of time, or your issue will be closed. See + CONTRIBUTING.md + +--- + +ISSUES THAT DO NOT FOLLOW THIS TEMPLATE WILL BE CLOSED IMMEDIATELY. + +- [ ] This is not a usage question. + - Our volunteers' time is limited, so please ask usage questions on + [StackOverflow](http://stackoverflow.com/questions/tagged/authlogic). +- [ ] This is not a security issue. + - Do not disclose security issues in public. See our [contributing + guide](https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md) + for instructions. +- [ ] I am committed to implementing this feature in a reasonable amount of + time, and responding promptly to feedback. + +# Current Behavior + +Describe. + +# Proposed Behavior + +Describe. + +# Proposed Solution + +It's OK if you don't have a solution, we can help with that. But, whatever +solution we decide on together, you must build yourself. diff --git a/.github/triage.md b/.github/triage.md new file mode 100644 index 00000000..e75ef037 --- /dev/null +++ b/.github/triage.md @@ -0,0 +1,93 @@ +# Triage + +Common responses to issues. + +## Ignored issue template, bug report + +Due to limited volunteers, all issues are required to use our [issue template](https://github.com/binarylogic/authlogic/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). Please open a new issue and follow the instructions. We can help, but we require you to take an active leadership role in fixing any issues you identify. If code changes are needed, you can open an issue for discussion, but you must commit to authoring a PR. + +## Usage Question + +Due to limited volunteers, we can't accept usage questions. Please ask such questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/authlogic). We don't even have enough volunteers to accept non-security bug reports, unless you can commit to fixing them yourself, and just need some help. If you can commit to fixing a non-security bug yourself, you can open an issue, but please follow our issue template. + +## Usage question we were able to answer + +``` +If that doesn't answer your question, please ask a new question +on [stackoverflow][1]. Unfortunatley, we just don't have enough volunteers to +handle usage questions on github. + +Also, please check the [reference documentation][2]. You might find something +there that's not in the readme. + +Thanks! + +[1]: http://stackoverflow.com/questions/tagged/authlogic +[2]: https://github.com/binarylogic/authlogic#1c-reference-documentation +``` + +## Old issue, generic + +``` +Hello, I'm going through old authlogic issues and seeing what to do with them. +Skimming through this, it's unclear if it's a usage question, a feature +suggestion, or a bug report. + +If this is a bug report, and you can still reproduce this issue with a clean +install of the latest version of authlogic and rails (currently 3.6.0 and 5.1.4 +respectively), please create a git repo with a sample app that reproduces the +problem, and open a new issue. + +If this is a feature suggestion, it's still relevant, and you are committed to +implementing it, please open a new issue and we can discuss your implementation +plan. + +If this is a usage question, please ask it on [stackoverflow][1]. Unfortunatley, +we just don't have enough volunteers to handle usage questions on github. Also, +please check the [reference documentation][2]. You might find something there +that's not in the readme. + +Thanks! + +[1]: http://stackoverflow.com/questions/tagged/authlogic +[2]: https://github.com/binarylogic/authlogic#1c-reference-documentation +``` + +## Old issue, usage question / feature suggestion + +``` +Hello, I'm going through old authlogic issues and seeing what to do with them. +This one looks a bit like a usage question and a bit like a feature suggestion. + +If this is a feature suggestion, it's still relevant, and you are committed to +implementing it, please open a new issue and we can discuss your implementation +plan. + +If this is a usage question, please ask it on [stackoverflow][1]. Unfortunately, +we just don't have enough volunteers to handle usage questions on github. Also, +please check the [reference documentation][2]. You might find something there +that's not in the readme. + +Thanks! + +[1]: http://stackoverflow.com/questions/tagged/authlogic +[2]: https://github.com/binarylogic/authlogic#1c-reference-documentation +``` + +## Old issue, bug report + +``` +Hello, I'm going through old authlogic issues and seeing what to do with them. +This one looks like a bug report. + +If you can still reproduce this issue with a clean install of the latest version +of authlogic and rails, and you are committed to fixing it, please open a new issue. + +If this was more of a usage question than a bug report, please ask your question +on [stackoverflow][1]. Unfortunately, we just don't have enough volunteers to +handle usage questions on github. + +Thanks! + +[1]: http://stackoverflow.com/questions/tagged/authlogic +``` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..487e5304 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,127 @@ +name: gha-workflow-authlogic-test +on: [push, pull_request] +jobs: + # Linting is a separate job, primary because it only needs to be done once, + # and secondarily because jobs are performed concurrently. + gha-job-authlogic-lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + # Set to `TargetRubyVersion` in `.rubocop.yml` + ruby-version: 2.6 + - name: Bundle + run: | + gem install bundler -v 2.4.22 + bundle install --jobs 4 --retry 3 + - name: Lint + run: bundle exec rubocop + + # The test job is a matrix of ruby/rails versions. + gha-job-authlogic-test: + name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }} + runs-on: ubuntu-latest + services: + gha-service-authlogic-mysql: + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: authlogic + image: mysql:8.0 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + ports: + - 3306:3306 + gha-service-authlogic-postgres: + env: + POSTGRES_PASSWORD: asdfasdf + image: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + strategy: + fail-fast: false + # Unlike TravisCI, the database will not be part of the matrix. Each + # sub-job in the matrix tests all three databases. Alternatively, we could + # have set this up with each database as a separate job, but then we'd be + # duplicating the matrix configuration three times. + matrix: + # To keep matrix size down, only test highest and lowest rubies. In + # `.rubocop.yml`, set `TargetRubyVersion`, to the lowest ruby version + # tested here. + ruby: ["3.1", "3.2", "3.3", "3.4"] + rails: ["7.0", "7.1", "7.2", "8.0"] + exclude: + # rails 7 requires ruby >= 2.7.0 + - rails: "7.0" + ruby: "3.1" + - rails: "7.0" + ruby: "3.2" + - rails: "7.0" + ruby: "3.3" + - rails: "7.0" + ruby: "3.4" + - rails: "8.0" + ruby: "3.1" + env: + BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.rb + steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Setup ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + # MySQL db was created above, sqlite will be created during test suite, + # when migrations occur, so we only need to create the postgres db. I + # tried something like `cd .....dummy_app && ....db:create`, but couldn't + # get that to work. + - name: Create postgres database + run: | + createdb \ + --host=$POSTGRES_HOST \ + --port=$POSTGRES_PORT \ + --username=postgres \ + authlogic + env: + PGPASSWORD: asdfasdf + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + + # The following three steps finally run the tests. + # We run `rake test` instead of the default task, which includes rubocop, + # because rubocop is done (once!) in a separate job above. + - name: Test, sqlite + run: bundle exec rake test + env: + DB: sqlite + - name: Test, mysql + run: bundle exec rake test + env: + BACKTRACE: 1 + DB: mysql + AUTHLOGIC_DB_NAME: authlogic + AUTHLOGIC_DB_USER: root + AUTHLOGIC_DB_HOST: 127.0.0.1 + AUTHLOGIC_DB_PORT: 3306 + - name: Test, postgres + run: bundle exec rake test + env: + BACKTRACE: 1 + DB: postgres + AUTHLOGIC_DB_NAME: authlogic + AUTHLOGIC_DB_USER: postgres + AUTHLOGIC_DB_HOST: 127.0.0.1 + AUTHLOGIC_DB_PORT: 5432 + AUTHLOGIC_DB_PASSWORD: asdfasdf diff --git a/.gitignore b/.gitignore index fe138eef..360d3279 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,15 @@ .DS_Store .swp +*.gem *.log *.sqlite3 pkg/* coverage/* -doc/* benchmarks/* -.specification +.rvmrc +gemfiles/*.lock +.bundle +Gemfile.lock +.ruby-gemset +.ruby-version +.byebug_history diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..c65bf98c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,137 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + Exclude: + # TravisCI runs `bundle install --path=${BUNDLE_PATH:-vendor/bundle}` + # causing our bundle to be installed in `gemfiles/vendor/bundle`. + # Regardless, we have no interest in linting files in our bundle :D + - gemfiles/vendor/bundle/**/* + # Specify lowest supported ruby version. If we committed our .ruby-version + # file, we wouldn't have to specify this (https://bit.ly/2vNTsue), but we + # don't commit that file because that would interfere with testing multiple + # rubies on CI. + # + # Should be same as `ruby-version` in `.github/workflows/test.yml` + TargetRubyVersion: 2.6 + +# Avoid empty lines in methods, they are a sign the method is too big. +Layout/EmptyLineAfterGuardClause: + Enabled: false + +# Aim for 80, but 100 is OK. +Layout/LineLength: + Max: 100 + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented + +Layout/MultilineOperationIndentation: + EnforcedStyle: indented + +# Please use normal indentation when aligning parameters. +# +# Good: +# +# method_call( +# a, +# b +# ) +# +# method_call(a, +# b +# ) +# +# Bad: +# +# method_call(a, +# b) +# +# The latter is harder to maintain and uses too much horizontal space. +Layout/ParameterAlignment: + EnforcedStyle: with_fixed_indentation + +Metrics/AbcSize: + Exclude: + # In an ideal world tests would be held to the same ABC metric as production + # code. In practice, time spent doing so is not nearly as valuable as + # spending the same time improving production code. + - test/**/* + +# Questionable value compared to metrics like AbcSize or CyclomaticComplexity. +Metrics/BlockLength: + Enabled: false + +# Questionable value compared to metrics like AbcSize or CyclomaticComplexity. +Metrics/ClassLength: + Enabled: false + +# Questionable value compared to metrics like AbcSize or CyclomaticComplexity. +Metrics/MethodLength: + Enabled: false + +# Questionable value compared to metrics like AbcSize or CyclomaticComplexity. +Metrics/ModuleLength: + Enabled: false + +# Sometimes prefixing a method name with get_ or set_ is a reasonable choice. +Naming/AccessorMethodName: + Enabled: false + +# Having a consistent delimiter, like EOS, improves reading speed. The delimiter +# is syntactic noise, just like a quotation mark, and inconsistent naming would +# hurt reading speed, just as inconsistent quoting would. +Naming/HeredocDelimiterNaming: + Enabled: false + +# Avoid single-line method definitions. +Style/EmptyMethod: + EnforcedStyle: expanded + +# Avoid annotated tokens except in desperately complicated format strings. +# In 99% of format strings they actually make it less readable. +Style/FormatStringToken: + Enabled: false + +# Too subtle to lint. Guard clauses are great, use them if they help. +Style/GuardClause: + Enabled: false + +# Too subtle to lint. A multi-line conditional may improve readability, even if +# a postfix conditional would satisfy `Metrics/LineLength`. +Style/IfUnlessModifier: + Enabled: false + +# Too subtle to lint. Use semantic style, but prefer `}.x` over `end.x`. +Style/BlockDelimiters: + Enabled: false + +# Use the nested style because it is safer. It is easier to make mistakes with +# the compact style. +Style/ClassAndModuleChildren: + EnforcedStyle: nested + +Style/Documentation: + Exclude: + - 'test/**/*' + +# Both `module_function` and `extend_self` are legitimate. Most importantly, +# they are different (http://bit.ly/2hSQAGm) +Style/ModuleFunction: + Enabled: false + +# `x > 0` is understood by more programmers than `x.positive?` +Style/NumericPredicate: + EnforcedStyle: comparison + +# Use slashes for most patterns. Use %r when it reduces backslash escaping. +Style/RegexpLiteral: + AllowInnerSlashes: false + +# We use words, like `$LOAD_PATH`, because they are much less confusing that +# arcane symbols like `$:`. Unfortunately, we must then `require "English"` in +# a few places, but it's worth it so that we can read our code. +Style/SpecialGlobalVars: + EnforcedStyle: use_english_names + +Style/StringLiterals: + EnforcedStyle: double_quotes diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 00000000..654499f1 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,64 @@ +# This configuration was generated by +# `rubocop --auto-gen-config --exclude-limit 1000` +# on 2020-03-24 00:02:43 -0400 using RuboCop version 0.80.1. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 7 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, IndentationWidth. +# SupportedStyles: with_first_argument, with_fixed_indentation +Layout/ArgumentAlignment: + Exclude: + - 'lib/authlogic/acts_as_authentic/magic_columns.rb' + - 'lib/authlogic/acts_as_authentic/single_access_token.rb' + - 'test/libs/user.rb' + +# Offense count: 14 +# Cop supports --auto-correct. +Lint/SendWithMixinArgument: + Exclude: + - 'lib/authlogic/acts_as_authentic/base.rb' + - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' + - 'lib/authlogic/test_case.rb' + +# Offense count: 3 +Metrics/AbcSize: + Max: 16.03 + +# Offense count: 5 +Style/ClassVars: + Exclude: + - 'lib/authlogic/i18n.rb' + +# Offense count: 4 +Style/MethodMissingSuper: + Exclude: + - 'lib/authlogic/controller_adapters/abstract_adapter.rb' + - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' + - 'lib/authlogic/test_case/mock_request.rb' + +# Offense count: 3 +Style/MissingRespondToMissing: + Exclude: + - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' + - 'lib/authlogic/test_case/mock_request.rb' + +# Offense count: 6 +# Cop supports --auto-correct. +# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. +# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym +Style/TrivialAccessors: + Exclude: + - 'lib/authlogic/session/base.rb' + +Style/HashEachMethods: + Enabled: false + +Style/HashTransformKeys: + Enabled: false + +Style/HashTransformValues: + Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2317c2b5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,139 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## Unreleased + +## 6.5.0 (2025-04-10) + +- Breaking Changes + - None +- Added + - None +- Fixed + - [#770](https://github.com/binarylogic/authlogic/pull/770) - Adds support for Rails 7.2 and 8.0 + - [#777](https://github.com/binarylogic/authlogic/pull/777) - Loads authlogic once Active Record has successfully loaded + +## 6.4.3 (2023-12-17) + +- Breaking Changes + - None +- Added + - Rubygems MFA requirement for authors added to gemspec. +- Fixed + - [#767](https://github.com/binarylogic/authlogic/pull/767) - Adds support for Rails 7.1 + - [#769](https://github.com/binarylogic/authlogic/issues/769) - Fixed GH workflow + +## 6.4.2 (2021-12-21) + +- Breaking Changes + - None +- Added + - None +- Fixed + - [#743](https://github.com/binarylogic/authlogic/pull/743) - Fixed + deprecation warning in Rails 7 re: `ActiveRecord::Base.default_timezone` + - [#745](https://github.com/binarylogic/authlogic/pull/745) - Fixed more + deprecation warnings in Rails 7 + +## 6.4.1 (2021-02-22) + +- Breaking Changes + - None +- Added + - `Authlogic::Session::Base.session_fixation_defense` - Reset the Rack + session ID after authentication, to protect against Session Fixation + attacks. (https://guides.rubyonrails.org/security.html#session-fixation) + Default: true +- Fixed + - None + +## 6.4.0 (2020-12-22) + +- Breaking Changes + - None +- Added + - [#734](https://github.com/binarylogic/authlogic/pull/734) - Support for + string cookies when using TestCase and friends +- Fixed + - None + +## 6.3.0 (2020-12-17) + +- Breaking Changes + - None +- Added + - [#733](https://github.com/binarylogic/authlogic/pull/733) - Rails 6.1 support + - `find_by_login_method` is deprecated in favor of `record_selection_method`, + to avoid confusion with ActiveRecord's "Dynamic Finders". +- Fixed + - [#726](https://github.com/binarylogic/authlogic/issues/726) - Thread + safety in `Authlogic::Session::Base.klass_name` + +## 6.2.0 (2020-09-03) + +- Breaking Changes + - None +- Added + - [#684](https://github.com/binarylogic/authlogic/pull/684) - Use cookies + only when available. Support for `ActionController::API` +- Fixed + - [#725](https://github.com/binarylogic/authlogic/pull/725) - `NoMethodError` + when setting `sign_cookie` or `encrypt_cookie` before `controller` is + defined. + +## 6.1.0 (2020-05-03) + +- Breaking Changes + - None +- Added + - [#666](https://github.com/binarylogic/authlogic/pull/666) - + Forwardported Authlogic::Session::Cookies.encrypt_cookie option + - [#723](https://github.com/binarylogic/authlogic/pull/723) - + Option to raise a `Authlogic::ModelSetupError` when your database is not + configured correctly. +- Fixed + - None + +## 6.0.0 (2020-03-23) + +- Breaking Changes, Major + + - There is no longer a default `crypto_provider`. We still recommend SCrypt, + but don't want users of other providers to be forced to install it. You + must now explicitly specify your `crypto_provider`, eg. in your `user.rb`. + + acts_as_authentic do |c| + c.crypto_provider = ::Authlogic::CryptoProviders::SCrypt + end + + To continue to use the `scrypt` gem, add it to your `Gemfile`. + + gem "scrypt", "~> 3.0" + +- Breaking Changes, Minor + - To set your crypto provider, you must use `crypto_provider=`, not + `crypto_provider`. The arity of the later has changed from -1 (one optional + arg) to 0 (no arguments). +- Added + - [#702](https://github.com/binarylogic/authlogic/pull/702) - The ability to + specify "None" as a valid SameSite attribute +- Fixed + - [#686](https://github.com/binarylogic/authlogic/pull/686) - Respect + the `log_in_after_create` setting when creating a new logged-out user + - [#668](https://github.com/binarylogic/authlogic/pull/668) - + BCrypt user forced to load SCrypt + - [#697](https://github.com/binarylogic/authlogic/issues/697) - Add V2 + CryptoProviders for MD5 and SHA schemes that fix key stretching by hashing + the byte digests instead of the hex strings representing those digests +- Dependencies + - Drop support for ruby 2.3 (reached EOL on 2019-04-01) + +## Previous major version + +See eg. the `5-1-stable` branch + +[1]: https://github.com/binarylogic/authlogic/blob/master/doc/use_normal_rails_validation.md diff --git a/CHANGELOG.rdoc b/CHANGELOG.rdoc deleted file mode 100644 index d7ecc7b6..00000000 --- a/CHANGELOG.rdoc +++ /dev/null @@ -1,345 +0,0 @@ -== 2.1.2 - -* Return the newly create object for the class level create method, instead of a boolean -* Add a model_name class method for Authlogic::Session for rails 3 compatibility. Will be using ActiveModel eventually, but this should be a quick fix. - -== 2.1.1 released 2009-7-04 - -* Use mb_chars when downcasing the login string to support international characters. -* Check for the existence of the :remember_me key before setting remember_me off of a hash. -* Added check to make sure Authlogic is not loaded too late, causing a NotActivated error. - -== 2.1.0 released 2009-6-27 - -* Fixed bug when using act_like_restful_authentication and setting passwords, needed to add a 2nd parameter to tell if to check against the database or not. -* Don't save record if they are read only. - -== 2.0.14 released 2009-6-13 - -* Fixed issue with using brute force protection AND generalize_credentials_error_messages. Brute force protection was looking to see if there were password errors, which generalize_credentials_error_messages was obfuscating. -* Added db_setup? method to avoid errors during rake tasks where the db might not be set up. Ex: migrations -* Stop using errors.on(key) since that is now deprecated in Rails. Use errors[key] instead. -* Use valid_password? for the method name to validate a password instead of valid_#{password_field}?. - -== 2.0.13 released 2009-5-13 - -* Add authlogic/regex.rb to manifest - -== 2.0.12 released 2009-5-13 - -* Added the ability to add a last_request_update_allowed? method in your controller to pragmatically tell Authlogic when and when not to update the last_request_at field in your database. This only takes effect if the method if present. -* Extracted Authlogic's regular expressions into it's own module to allow easy use of them outside of Authlogic. See Authlogic::Regex for more info. -* Made being_brute_force_protected? true for the Authlogic::Session::BruteForceProtection module. -* Added the configuration option generalize_credentials_error_messages for the Authlogic::Session::Password module. This allows you to generalize your login / password errors messages as to not reveal was the problem was when authenticating. If enabled, when an invalid login is supplied it will use the same exact error message when an invalid password is supplied. -* Update email regular expression to use A-Z0-9 instead of /w as to not allow for diacritical marks in an email address. -* Changed config() convenience method to rw_config() to be more descriptive and less vague. - -== 2.0.11 released 2009-4-25 - -* Fix bug when password is turned off and the SingleAccessToken module calls the after_password_set callback. -* HTTP basic auth can now be toggled on or off. It also checks for the existence of a standard username and password before enabling itself. -* Added option check_passwords_against_database for Authlogic::ActsAsAuthentic::Password to toggle between checking the password against the database value or the object value. Also added the same functionality to the instance method: valid_password?("password", true), where the second argument tells Authlogic to check the password against the database value. The default for this new feature is true. -* Add a maintain_sessions configuration option to Authlogic::ActsAsAuthentic::SessionMaintenance as a "clearer" option to disable automatic session maintenance. -* single_access_allowed_request_types can also be equal to :all instead of just [:all]. -* Refactor params_enabled? so that the single_access_allowed? method in controllers takes precedence. -* Added testing comments in the README and expanded on the documentation in Authlogic::TestCase - -== 2.0.10 released 2009-4-21 - -* Mock request is now transparent to non existent methods. Since the methods calls really have no functional value when testing authlogic. -* Allow password confirmation to be disabled. -* Modified login format validation to allow for the + character since emails addresses allow that as a valid character. -* Added merge_* configuration methods for acts_as_authentic to make merging options into configuration options that default to hashes. Just a few convenience methods. - -== 2.0.9 released 2009-4-9 - -* Fixed bug where hooks provided by the password module were called when the password module was not being used due to the fact that the password field did not exist. -* Fixed bug where the find_with_login method was not being aliased if you were using an alternate field besides login. - -== 2.0.8 release 2009-4-9 - -* Dont reset the @password_changed instance variable to false because its halts the callback chain, instead reset it to nil. - -== 2.0.7 released 2009-4-9 - -* Rename TestCase::ControllerAdapter to TestCase::RailsRequestAdapter to help clarify it's usage and fix a constant typo. - -== 2.0.6 released 2009-4-9 - -* Don't use second, use [1] instead so older rails versions don't complain. -* Update email regular expression to be less TLD specific: (?:[A-Z]{2,4}|museum|travel) -* Update shoulda macro for 2.0 -* validates_length_of_password_confirmation_field_options defaults to validates_confirmation_of_password_field_options -* Use MockCookieJar in tests instead of a Hash in the MockController. -* Cookies now store the record id as well, for faster lookup. Also to avoid the need to use sessions since sessions are lazily loaded in rails 2.3+ -* Add configuration option for Authlogic::ActsAsAuthentic: ignore_blank_passwords -* Fix cookie_domain in rails adapter -* Make password and login fields optional. This allows you to have an alternate authentication method as your main authentication source. Such as OpenID, LDAP, or whatever you want. -* Reset the @password_changed instance variable after the record has been saved. -* Add referer and user_agent to mock requests for testing purposes. -* Add :case_sensitive => false to validates_uniqueness_of calls on the login and email fields. -* MockRequest not tries to use controller.env['REMOTE_ADDR'] for the IP address in tests. -* Add in custom find_with_email and find_with_login methods to perform case insensitive searches for databases that are case sensitive by default. This is only done if the :case_insensitive option for validates_uniqueness_of_login_field_options or validates_uniqueness_of_email_field_options is set to false. Which, as of this version, it is. If you are using MySQL this has been the default behavior all along. If you are using SQLite or Postgres this has NOT been the default behavior. -* Added in exception explaining that you are using the old configuration for acts_as_authentic with an example of the new format. - -== 2.0.5 released 2009-3-30 - -* Stub out authenticate_with_http_basic for TestCase::ControllerAdapter. -* Added second parameter for add_acts_as_authentic module to specify the position: append or prepend. - -== 2.0.4 released 2009-3-28 - -* Added validates_uniqueness_of_login_field_options and validates_uniqueness_of_email_field_options configuration options -* Add in checks to make sure session_class is not nil. -* Cleaned up TestCase some more and added functionality to log users in during functional tests. - -== 2.0.3 released 2009-3-26 - -* Fixed error where default session class does not exist. -* Fixed human_name for the model to use its own human name and not delegate to the associated model. Translation should be under authlogic.models.user_session (or whatever the name of your session is). -* Fixed human_attribute_name to use Authlogic keys for translation instead of ActiveRecord: authlogic.attributes.user_session.login -* For transitioning from restful_authentication, set the REST_AUTH_SITE_KEY to '' if it doesn't exist, instead of nil. -* Completely rewrote Authlogic::Testing, it's now called Authlogic::TestCase. Testing Authlogic is much easier now. Please see Authlogic::TestCase for more info. - -== 2.0.2 released 2009-3-24 - -* Reset failed_login_count if consecutive_failed_logins_limit has been exceed and the failed_login_ban_for has passed. -* Update test helpers to use the new configuration scheme. -* Fixed issue when logging doesn't update last_request_at, so the next persistence try would fail. - -== 2.0.1 released 2009-3-23 - -* Validate length of password. -* Dont save sessions with a ! during session maintenance. -* Add self_and_descendants_from_active_record for Rails 2.3 -* Abort acts_as_authentic if there is no DB connection or table. - -== 2.0.0 released 2009-3-23 - -* Refactored nearly all code and tests, especially acts_as_authentic. Got rid of the meta programming and rewrote to use modules and hooks. Also moved all configuration into their related modules. -* Set up a strong API with hooks to allow you to modify behavior and most importantly, easily create "add on" modules or alternate authentication methods, etc. -* Changed configuration method for acts_as_authentic to accept a block instead of a hash. -* The record attribute will NEVER be set until after validation passes, similar to how ActiveRecord executes UPDATEs and CREATEs. -* Fixed bug with session maintenance where user would log in as new user when creating another user account, typically an admin function. -* Brute force protection is only a temporary ban by default, not a permanent one. -* Switched to Hoe for gem management instead of Echoe. -* Added MD5 crypto provider for legacy systems. -* Make password salt field optional for legacy systems. - -== 1.4.4 released 2009-3-2 - -* Moved session maintenance to a before_save, to save on queries executed and to skip an unexpected / additional save on the user object. -* Extracted random string generation into its own class and leverages SecureRandom if it is available -* Move cookies to a higher priority when trying to find the record to help with performance since Rails 3 lazily loads the sessions -* Reset perishable token in a before_save instead of a before_validation - -== 1.4.3 released 2009-2-22 - -* Fixed issue with brute force protection. - -== 1.4.2 released 2009-2-20 - -* Cleaned up callbacks system to use hooks and execute in the proper order. -* Added brute force protection. See the consecutive_failed_logins_limit configuration option in Authlogic::Session::Config. Also see Authlogic::Session:BruteForceProtection -* Fixed issue with calling stale? when there is no record. -* Simon Harris fixed the issue of using lock_version with the associated record and also optimized the library for better performance. -* Implemented saving the record during the callback chain to execute as few queries as possible. This way modules can hook into Authlogic, modify the associated record, and not have to worry about saving the record. - -== 1.4.1 released 2009-2-8 - -* Fixed I18n key misspelling. -* Added I18n keys for ORM error messages. -* Use the password_field configuration value for the alias_methods defined in acts_as_authentic/credentials.rb -* Change shoulda macros implementation to follow the shoulda documentation -* Rails >2.3 uses :domain for the session option instead of :session_domain. Authlogic now uses the proper key in the rails adapter. -* Added validate_password attribute to force password validation regardless if the password is blank. This is useful for forms explicitly changing passwords. -* The class level find method will return a session object if the session is stale. The protection is that there will be no record associated with that session. This allows you to receive an object and call the stale? method on it to determine why the user must log back in. -* Added validate callbacks in Session::Base so you can run callbacks by calling validate :my_method, just like in AR. -* Checked for blank persistence tokens when trying to validate passwords, this is where transitioning occurs. People transitioning from older systems never had a persistence token, which means it would be nil here. -* Update allowed domain name extensions for email -* Ignore default length options for validations if alternate length options are provided, since AR raises an error if 2 different length specifications are provided. - -== 1.4.0 released 2009-1-28 - -* Added support for cookie domain, based on your frameworks session domain configuration -* Updated test helper functions to use the persistence token config value -* Check for UTC times when using Time.now for current_login_at and last_request_at -* Single access now looks for a single_access_allowed? method in your controllers to determine if single access should be allowed or not. Allowing you to define exactly when single access is allowed. -* Finding the authenticated record uses klass.primary_key instead of assuming id. -* BREAKS BACKWARDS COMPATIBILITY: New I18n solution implemented. See Authlogic::I18n for more information. - -== 1.3.9 released 2009-1-9 - -* Added the disable_perishable_token_maintenance option to disable the automatic resetting of the perishable_token, meaning you will have to maintain this yourself. -* Changed shoulda macro to conform to standards so model is not required to be passed -* Modified method definitions for the Session class to check for already defined methods, allowing you to write your own "credential" methods, and Authlogic will not overwrite your custom methods. -* Fixed bug when passing :all to single_access_allowed_request_types -* Added logout_on_timeout configuration option for Session::Base - -== 1.3.8 released 2008-12-24 - -* Only change persistence token if the password is not blank -* Normalize the last_request_at_threshold so that you can pass an integer or a date/time range. -* Fixed bug where password length validations were not being run because the password value was not blank. It should be run if it is a new record, the password has changed, or the password is blank. -* Added disable_magic_states option for sessions, to turn off the automatic checking of "magic states" such as active?, confirmed?, and approved?. - -== 1.3.7 released 2008-11-30 - -* Added session generator: script/generate session UserSession -* Added Test::Unit helpers file, see testing in the README - -== 1.3.6 released 2008-11-30 - -* Modified validates_length_of for password so that there is a fallback validation if the passed "if statement" fails - -== 1.3.5 released 2008-11-30 - -* :transition_from_crypto_provider for acts_as_authentic now accepts an array to transition from multiple providers. Which solves the problem of a double transition. -* Added AES256 as a crypto_provider option, for those that want to use a reversible encryption method by supplying a key. -* Fixed typo for using validates_format_of_options instead of validates_length_of_options -* Fixed bug when accessing the dynamic method for accessing the session record in a namespace, since it uses class_name.underscore which replaces :: with a / -* Added minimum length requirement of 4 for the password, and removed validates_presence_of for password since validates_length_of enforces this -* Set before_validation to reset the persistence token if it is blank, since a password is not required for open id authentication - -== 1.3.4 released 2008-11-24 - -* Delegate human_attribute_name to the ActiveRecord class to take advantage of the I18n feature. -* Fixed issue with passwords from older versions of restful_authentication, the passwords end with -- - -== 1.3.3 released 2008-11-23 - -* Updated :act_like_restful_authentication for those using the older version where no site wide key is preset (REST_AUTH_SITE_KEY), Authlogic will adjust automatically based on the presence of this constant. -* Added :transition_from_crypto_provider option for acts_as_authentic to transition your user's passwords to a new algorithm. -* Added :transition_from_restful_authentication for acts_as_authentic to transition your users from restful_authentication to the Authlogic password system. Now you can choose to keep your passwords the same by using :act_like_restful_authentication, which will *NOT* do any transitioning, or you can use :transition_from_crypto_provider which will update your users passwords as they login or new accounts are created, while still allowing users with the old password system to log in. -* Modified the "interface" for the crypto providers to only provide a class level encrypt and matches? method, instead of a class level encrypt and decrypt method. - -== 1.3.2 released 2008-11-22 - -* Updated code to work better with BCrypt, using root level class now. - -== 1.3.1 released 2008-11-22 - -* Fixed typo in acts_as_authentic config when passing the :scope option. -* Added :act_like_restful_authentication option for acts_as_authentic -* Added a new crypto provider: BCrypt, this is for those storing the nuclear launch codes in their apps - -== 1.3.0 released 2008-11-21 - -* BREAKS BACKWARDS COMPATIBILITY: changed the confirm_password field to password_confirmation for acts_as_authentic, since the rails validates_confirmation_of handles creating this attribute and there is no option to change the name of this. -* BREAKS BACKWARDS COMPATIBILITY: Cleaned up all of the validation configuration for acts_as_authentic, as well as the documentation that goes with it, you can accomplish the same things as before, but this is much more flexible and much more organized. This is mainly for those implementing i18n support. Instead of :whatever_message, its now :login_field_validates_length_of_options => {:message => "your i18n friendly message"}. As a side note, with the new i18n support in rails I would not be surprised if this is already done for you since Authlogic uses the ActiveRecord validation methods. -* Got rid of simple delegator for the abstract controller, apparently this has performance issues. -* Cleaned up validations to assume ActiveRecord dirty attributes are present, I think this is a safe assumption. - -== 1.2.2 released 2008-11-20 - -* Added allow_blank_login_and_password_field and allow_blank_email_field options to acts_as_authentic, which allows you to have alternative logins, such as OpenID -* In the session Authlogic now also stores the record id. We use this id to find the record and then check the token against the record, thus allowing for quicker database lookups, while getting the same security. -* Skip validation for reset_perishable_token! -* Added checks for uniqueness validations to only perform if the values have changed, this cuts down on DB queries -* Abstract controller adapter now uses ruby's simple delegator class -* Allow to save with a block: user_session.save { |result| }, result will either be false or self, this is useful when implementing OpenID and other methods - -== 1.2.1 released 2008-11-19 - -* Added build method to authenticates_many association to act like AR association collections. -* Added validation boolean configuration options for acts_as_authentic: validate_field, validate_login_field, validate_password_field, validate_email_field. This turns on and off validations for their respective fields. -* Renamed all password_reset_token terms to perishable_token, including configuration, etc. I still allow for the old configurations so this will not break compatibility, but perishable token is a better name and can be used for account confirmation as well as a password reset token, or anything else you want. -* Renamed all remember_token instances to persistence_token, the term "remember token" doesn't really make sense. I still allow for the old configuration, so this will not break backwards compatibility: persistence_token fits better and makes more sense. - -== 1.2.0 released 2008-11-16 - -* Added check for database set up in acts_as_authentic to prevent errors during migrations. -* Forced logged_in and logged_out named scopes to use seconds. -* Hardened valid_password? method to only allow raw passwords. -* controllers and scopes are no longer stored in class variables but in the Thread.current hash so their instances die out with the thread, which frees up memory. -* Removed single_access_token_field and remember_token_field from Sesson::Config, they are not needed there. -* Added password_reset_token to assist in resetting passwords. -* Added email_field, email_field_regex, email_field_regex_failed_message configuration options to acts_as_authentic. So that you can validate emails as well as a login, instead of the either-or approach. -* Added configuration for all validation messages for the session so that you can modify them and provide I18n support. - -== 1.1.1 released 2008-11-13 - -* Removed ActiveRecord dependency. -* Removed loading shoulda macros by default, moved to shoulda_macros dir. -* Modified how params access works. Added in single_access_token_field which params now uses. See the single access section in the README. Various configuration options added as well. -* Cleaned up acts_as_authentic configuration, added new config module to do this. -* Cleaned up acts_as_authentic tests -* Moved acts_as_authentic sub modules into the proper name spaces - -== 1.1.0 released 2008-11-13 - -* Moved Rack standards into abstract_adapter for the controllers. -* Added authenticating_with_credentials?, authenticating_with_unauthorized_record? -* Fixed typo in abstract_adapter, black to block. -* Cleaned up / reorganized tests. -* Moved ActiveRecord additions to ORM Adapters name space to make way for Data Mapper. -* Reorganized and modified acts_as_authentic to be free standing and not get info from the related session. -* The session now gets its configuration from the model, since determining which fields are present is ORM specific. -* Extracted session and cookie logic into their own modules for Session. -* Moved crypto providers into their own module and added a Sha1 provider to help with the restful_authentication transition. -* Allow the unique_token method to use the alternate crypto_provider if it is a hash algorithm, otherwise default to Sha512. -* Added last_request_at_threshold configuration option. -* Changed Scoped class to AuthenticatesManyAssociation, like AR has HasManyAssociation, etc. -* Added should_be_authentic shoulda macro. -* Removed some magic from how sessions are initialized. See the initialize documentation, this method is a little more structured now, which was required for adding in openid. -* Added in logging via a params token, which is friendly for feed URLs. Works just like cookies and sessions when persisting the session. -* Added the option to use session.user, instead of session.record. This is based off of what model your session is authenticating with. - -== 1.0.0 released 2008-11-05 - -* Checked for blank login counts, if a default wasnt set in the migrations. -* Added check for database table in acts_as_authentic to avoid errors in initial setup. -* Completely rewrote tests to be more conventional and thorough tests, removed test_app. -* Modified how validations work so that a validate method was added as well as callbacks for that method. -* Extracted scope support into its own module to help organize code better. -* Added in salt for encryption, just like hashes and removed :crypto_provider_type option for acts_as_authentic. -* Added merb adapters. -* Improved documentation throughout. - -== 0.10.4 released 2008-10-31 - -* Changed configuration to use inheritable attributes -* Cleaned up requires to be in their proper files -* Added in scope support. - -== 0.10.3 released 2008-10-31 - -* Instead of raising an error when extra fields are passed in credentials=, just ignore them. -* Added remember_me config option to set the default value. -* Only call credential methods if an argument was passed. -* More unit tests -* Hardened automatic session updating. Also automatically log the user in if they change their password when logged out. - -== 0.10.2 released 2008-10-24 - -* Added in stretches to the default Sha512 encryption algorithm. -* Use column_names instead of columns when determining if a column is present. -* Improved validation callbacks. after_validation should only be run if valid? = true. Also clear errors before the "before_validation" callback. - -== 0.10.1 released 2008-10-24 - -* Sessions now store the "remember token" instead of the id. This is much safer and guarantees all "sessions" that are logged in are logged in with a valid password. This way stale sessions can't be persisted. -* Bumped security to Sha512 from Sha256. -* Remove attr_protected call in acts_as_authentic -* protected_password should use pasword_field configuration value -* changed magic state "inactive" to "active" - -== 0.10.0 released 2008-10-24 - -* Do not allow instantiation if the session has not been activated with a controller object. Just like ActiveRecord won't let you do anything without a DB connection. -* Abstracted controller implementation to allow for rails, merb, etc adapters. So this is not confined to the rails framework. -* Removed create and update methods and added save, like ActiveRecord. -* after_validation should be able to change the result if it adds errors on callbacks. -* Completed tests. - -== 0.9.1 released 2008-10-24 - -* Changed scope to id. Makes more sense to call it an id and fits better with the ActiveRecord model. -* Removed saving_from_session flag, apparently it is not needed. -* Fixed updating sessions to make more sense and be stricter. -* change last_click_at to last_request_at -* Only run "after" callbacks if the result is successful. - -== 0.9.0 released 2008-10-24 - -* Initial release. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d5cb3118 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,137 @@ +# Contributing to Authlogic + +## Issues + +### Security Issues + +**Do not disclose security issues in public.** Instead, please email: + +``` +Ben Johnson , +Tieg Zaharia , +Jared Beck +``` + +We will review security issues promptly. + +### Non-Security Issues + +Please use github issues only for bug reports and feature suggestions. + +### Usage Questions + +Please ask usage questions on +[Stack Overflow](http://stackoverflow.com/questions/tagged/authlogic). + +## Development + +Most local development should be done using the oldest supported version of +ruby. See `required_ruby_version` in the gemspec. + +### Testing + +Tests can be run against different versions of Rails: + +``` +# Rails 5.2 +BUNDLE_GEMFILE=gemfiles/rails_5.2.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_5.2.rb bundle exec rake + +# Rails 6.0 +BUNDLE_GEMFILE=gemfiles/rails_6.0.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_6.0.rb bundle exec rake + +# Rails 6.1 +BUNDLE_GEMFILE=gemfiles/rails_6.1.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_6.1.rb bundle exec rake + +# Rails 7.0 +BUNDLE_GEMFILE=gemfiles/rails_7.0.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_7.0.rb bundle exec rake + +# Rails 7.1 +BUNDLE_GEMFILE=gemfiles/rails_7.1.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_7.1.rb bundle exec rake + +# Rails 7.2 +BUNDLE_GEMFILE=gemfiles/rails_7.2.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_7.2.rb bundle exec rake + +# Rails 8.0 +BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle install +BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake +``` + +To run a single test: + +``` +BUNDLE_GEMFILE=gemfiles/rails_8.0.rb \ + bundle exec ruby -I test path/to/test.rb +``` + +Bundler can be omitted, and the latest installed version of a gem dependency +will be used. This is only suitable for certain unit tests. + +``` +ruby –I test path/to/test.rb +``` + +### Test MySQL + +``` +mysql -e 'drop database authlogic; create database authlogic;' && \ + DB=mysql BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake +``` + +### Test PostgreSQL + +``` +psql -c 'create database authlogic;' -U postgres +DB=postgres BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake +``` + +### Linting + +Running `rake` also runs a linter, rubocop. Contributions must pass both +the linter and the tests. The linter can be run on its own. + +``` +BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rubocop +``` + +To run the tests without linting, use `rake test`. + +``` +BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake test +``` + +### Version Control Branches + +We've been trying to follow the rails way, stable branches, but have been +inconsistent. We should have one branch for each minor version, named like +`4-3-stable`. Releases should be done on those branches, not in master. So, +the "stable" branches should be the only branches with release tags. + +### A normal release (no backport) + +1. git checkout 4-3-stable # the latest "stable" branch (see above) +1. git merge master +1. Update version number in lib/authlogic/version.rb +1. In the changelog, + - Add release date to entry + - Add a new "Unreleased" section at top +1. In the readme, + - Update version number in the docs table at the top + - For non-patch versions, update the compatibility table +1. Commit with message like "Release 4.3.0" +1. git push origin 4-3-stable +1. CI should pass +1. gem build authlogic.gemspec +1. gem push authlogic-4.3.0.gem +1. git tag -a -m "v4.3.0" "v4.3.0" +1. git push --tags origin 4-3-stable +1. update the docs in the master branch, because that's what people look at + - git checkout master + - git merge --ff-only 4-3-stable + - optional: amend commit, adding `[ci skip]` + - git push diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..bb94df82 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec diff --git a/LICENSE b/LICENSE index e23646c5..d9b6bd7c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009 Ben Johnson of Binary Logic +Copyright (c) 2011 Ben Johnson of Binary Logic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md new file mode 100644 index 00000000..d8c621f7 --- /dev/null +++ b/README.md @@ -0,0 +1,519 @@ +# Authlogic + +An unobtrusive ruby authentication library based on ActiveRecord. + +[![Gem Version][5]][6] [![Build Status][1]][2] [![Code Climate][7]][8] [![Dependency Status][3]][4] + +[![Coverage Status](https://coveralls.io/repos/github/binarylogic/authlogic/badge.svg?branch=master)](https://coveralls.io/github/binarylogic/authlogic?branch=master) + +## Documentation + +| Version | Documentation | +| ---------- | ----------------------------------------------------------------- | +| Unreleased | https://github.com/binarylogic/authlogic/blob/master/README.md | +| 6.5.0 | https://github.com/binarylogic/authlogic/blob/v6.5.0/README.md | +| 6.4.3 | https://github.com/binarylogic/authlogic/blob/v6.4.3/README.md | +| 5.2.0 | https://github.com/binarylogic/authlogic/blob/v5.2.0/README.md | +| 4.5.0 | https://github.com/binarylogic/authlogic/blob/v4.5.0/README.md | +| 3.7.0 | https://github.com/binarylogic/authlogic/blob/v3.7.0/README.md | +| 2.1.11 | https://github.com/binarylogic/authlogic/blob/v2.1.11/README.rdoc | +| 1.4.3 | https://github.com/binarylogic/authlogic/blob/v1.4.3/README.rdoc | + +## Table of Contents + +- [1. Introduction](#1-introduction) + - [1.a. Overview](#1a-overview) + - [1.b. Reference Documentation](#1b-reference-documentation) + - [1.c. Installation](#1c-installation) +- [2. Rails](#2-rails) + - [2.a. The users table](#2a-the-users-table) + - [2.b. Controller](#2b-controller) + - [2.b.1. Helper Methods](#2b1-helper-methods) + - [2.b.2. Routes](#2b2-routes) + - [2.b.3. ActionController::API](#2b3-actioncontroller-api) + - [2.c. View](#2c-view) + - [2.d. CSRF Protection](#2d-csrf-protection) +- [3. Testing](#3-testing) +- [4. Helpful links](#4-helpful-links) +- [5. Add-ons](#5-add-ons) +- [6. Internals](#6-internals) +- [7. Extending](#7-extending) +- [90. Compatibility](#90-compatibility) + +## 1. Introduction + +### 1.a. Overview + +Authlogic introduces a new type of model. You can have as many as you want, and +name them whatever you want, just like your other models. In this example, we +want to authenticate with our `User` model, which is inferred from the name: + +```ruby +class UserSession < Authlogic::Session::Base + # specify configuration here, such as: + # logout_on_timeout true + # ...many more options in the documentation +end +``` + +In a `UserSessionsController`, login the user by using it just like your other models: + +```ruby +UserSession.create(:login => "bjohnson", :password => "my password", :remember_me => true) + +session = UserSession.new(:login => "bjohnson", :password => "my password", :remember_me => true) +session.save + +# requires the authlogic-oid "add on" gem +UserSession.create(:openid_identifier => "identifier", :remember_me => true) + +# skip authentication and log the user in directly, the true means "remember me" +UserSession.create(my_user_object, true) +``` + +The above handles the entire authentication process for you by: + +1. authenticating (i.e. **validating** the record) +2. sets up the proper session values and cookies to persist the session (i.e. **saving** the record). + +You can also log out (i.e. **destroying** the session): + +```ruby +session.destroy +``` + +After a session has been created, you can persist it (i.e. **finding** the +record) across requests. Thus keeping the user logged in: + +```ruby +session = UserSession.find +``` + +To get all of the nice authentication functionality in your model just do this: + +```ruby +class User < ApplicationRecord + acts_as_authentic do |c| + c.my_config_option = my_value + end # the configuration block is optional +end +``` + +It is also "smart" in the sense that if a login or username field +is present it will use that to authenticate, if not it will look for +an email field. This is all configurable, but for 99% of cases the above +is all you will need to do. + +You may specify how passwords are cryptographically hashed (or encrypted) by +setting the Authlogic::CryptoProvider option: + +```ruby +c.crypto_provider = Authlogic::CryptoProviders::BCrypt +``` + +Also, sessions are automatically maintained. You can switch this on and off with +configuration, but the following will automatically log a user in after a +successful registration: + +```ruby +User.create(params[:user]) +``` + +You can switch this on and off with the following configuration: + +```ruby +class User < ApplicationRecord + acts_as_authentic do |c| + c.log_in_after_create = false + end # the configuration block is optional +end +``` + +Authlogic also updates the session when the user changes his/her password. You can also switch this on and off with the following configuration: + +```ruby +class User < ApplicationRecord + acts_as_authentic do |c| + c.log_in_after_password_change = false + end # the configuration block is optional +end +``` + +Authlogic is very flexible, it has a strong public API and a plethora of hooks +to allow you to modify behavior and extend it. Check out the helpful links below +to dig deeper. + +### 1.b. Reference Documentation + +This README is just an introduction, but we also have [reference +documentation](http://www.rubydoc.info/github/binarylogic/authlogic). + +**To use the reference documentation, you must understand how Authlogic's +code is organized.** There are 2 models, your Authlogic model and your +ActiveRecord model: + +1. **Authlogic::Session**, your session models that + extend `Authlogic::Session::Base`. +2. **Authlogic::ActsAsAuthentic**, which adds in functionality to your + ActiveRecord model when you call `acts_as_authentic`. + +### 1.c. Installation + +To install Authlogic, add this to your Gemfile: + +`gem 'authlogic'` + +And run `bundle install`. + +## 2. Rails + +Let's walk through a typical rails setup. ([Compatibility](#90-compatibility)) + +### 2.a.1 The users table + +If you want to enable all the features of Authlogic, a migration to create a +`User` model might look like this: + +```ruby +class CreateUser < ActiveRecord::Migration + def change + create_table :users do |t| + # Authlogic::ActsAsAuthentic::Email + t.string :email + t.index :email, unique: true + + # Authlogic::ActsAsAuthentic::Login + t.string :login + + # Authlogic::ActsAsAuthentic::Password + t.string :crypted_password + t.string :password_salt + + # Authlogic::ActsAsAuthentic::PersistenceToken + t.string :persistence_token + t.index :persistence_token, unique: true + + # Authlogic::ActsAsAuthentic::SingleAccessToken + t.string :single_access_token + t.index :single_access_token, unique: true + + # Authlogic::ActsAsAuthentic::PerishableToken + t.string :perishable_token + t.index :perishable_token, unique: true + + # See "Magic Columns" in Authlogic::Session::Base + t.integer :login_count, default: 0, null: false + t.integer :failed_login_count, default: 0, null: false + t.datetime :last_request_at + t.datetime :current_login_at + t.datetime :last_login_at + t.string :current_login_ip + t.string :last_login_ip + + # See "Magic States" in Authlogic::Session::Base + t.boolean :active, default: false + t.boolean :approved, default: false + t.boolean :confirmed, default: false + + t.timestamps + end + end +end +``` + +In the `User` model, + +```ruby +class User < ApplicationRecord + acts_as_authentic + + # Validate email, login, and password as you see fit. + # + # Authlogic < 5 added these validation for you, making them a little awkward + # to change. In 4.4.0, those automatic validations were deprecated. See + # https://github.com/binarylogic/authlogic/blob/master/doc/use_normal_rails_validation.md + validates :email, + format: { + with: /@/, + message: "should look like an email address." + }, + length: { maximum: 100 }, + uniqueness: { + case_sensitive: false, + if: :will_save_change_to_email? + } + + validates :login, + format: { + with: /\A[a-z0-9]+\z/, + message: "should use only letters and numbers." + }, + length: { within: 3..100 }, + uniqueness: { + case_sensitive: false, + if: :will_save_change_to_login? + } + + validates :password, + confirmation: { if: :require_password? }, + length: { + minimum: 8, + if: :require_password? + } + validates :password_confirmation, + length: { + minimum: 8, + if: :require_password? + } +end +``` + +### 2.a.2. UserSession model + +And define a corresponding model in `app/models/user_session.rb`: + +```ruby +class UserSession < Authlogic::Session::Base +end +``` + +### 2.b. Controller + +Your sessions controller will look just like your other controllers. + +```ruby +class UserSessionsController < ApplicationController + def new + @user_session = UserSession.new + end + + def create + @user_session = UserSession.new(user_session_params.to_h) + if @user_session.save + redirect_to root_url + else + render :new, status: 422 + end + end + + def destroy + current_user_session.destroy + redirect_to new_user_session_url + end + + private + + def user_session_params + params.require(:user_session).permit(:login, :password, :remember_me) + end +end +``` + +As you can see, this fits nicely into the [conventional controller methods][9]. + +#### 2.b.1. Helper Methods + +```ruby +class ApplicationController < ActionController::Base + helper_method :current_user_session, :current_user + + private + def current_user_session + return @current_user_session if defined?(@current_user_session) + @current_user_session = UserSession.find + end + + def current_user + return @current_user if defined?(@current_user) + @current_user = current_user_session && current_user_session.user + end +end +``` + +#### 2.b.2. Routes + +```ruby +Rails.application.routes.draw do + # ... + resources :users + resource :user_session +end +``` + +#### 2.b.3. ActionController::API + +> Because ActionController::API does not include ActionController::Cookies +> metal and ActionDispatch::Cookies rack module, Therefore, our controller can +> not use the cookies method. +> +> - [#684](https://github.com/binarylogic/authlogic/pull/684). + +### 2.c. View + +For example, in `app/views/user_sessions/new.html.erb`: + +```erb +<%= form_for @user_session, url: user_session_url do |f| %> + <% if @user_session.errors.any? %> +
+

<%= pluralize(@user_session.errors.count, "error") %> prohibited:

+
    + <% @user_session.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+ <% end %> + <%= f.label :login %>
+ <%= f.text_field :login %>
+
+ <%= f.label :password %>
+ <%= f.password_field :password %>
+
+ <%= f.label :remember_me %>
+ <%= f.check_box :remember_me %>
+
+ <%= f.submit "Login" %> +<% end %> +``` + +### 2.d. CSRF Protection + +Because Authlogic introduces its own methods for storing user sessions, the CSRF +(Cross Site Request Forgery) protection that is built into Rails will not work +out of the box. + +No generally applicable mitigation by the authlogic library is possible, because +the instance variable you use to store a reference to the user session in `def +current_user_session` will not be known to authlogic. + +You will need to override `ActionController::Base#handle_unverified_request` to +do something appropriate to how your app handles user sessions, e.g.: + +```ruby +class ApplicationController < ActionController::Base + ... + protected + + def handle_unverified_request + # raise an exception + fail ActionController::InvalidAuthenticityToken + # or destroy session, redirect + if current_user_session + current_user_session.destroy + end + redirect_to root_url + end +end +``` + +### 2.e. SameSite Cookie Attribute + +The SameSite attribute tells browsers when and how to fire cookies in first- or third-party situations. SameSite is used by a variety of browsers to identify whether or not to allow a cookie to be accessed. + +Up until recently, the standard default value when SameSite was not explicitly defined was to allow cookies in both first- and third-party contexts. However, starting with Chrome 80+, the SameSite attribute will not default to Lax behavior meaning cookies will only be permitted in first-party contexts. + +Authlogic can allow you to explicitly set the value of SameSite to one of: Lax, Strict, or None. Note that when setting SameSite to None, the `secure` flag must also be set (secure is the default in Authlogic). + +Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#SameSite + +## 3. Testing + +See [Authlogic::TestCase](https://github.com/binarylogic/authlogic/blob/master/lib/authlogic/test_case.rb) + +## 4. Helpful links + +- API Reference: http://www.rubydoc.info/github/binarylogic/authlogic +- Repository: https://github.com/binarylogic/authlogic/tree/master +- Railscasts Screencast: http://railscasts.com/episodes/160-authlogic +- Example repository with tutorial in README: https://github.com/binarylogic/authlogic_example/tree/master +- Tutorial: Rails Authentication with Authlogic https://www.sitepoint.com/rails-authentication-with-authlogic +- Issues: https://github.com/binarylogic/authlogic/issues +- Chrome is not logging out on browser close https://productforums.google.com/forum/#!topic/chrome/9l-gKYIUg50/discussion + +## 5. Add-ons + +- Authlogic OpenID addon: https://github.com/binarylogic/authlogic_openid +- Authlogic LDAP addon: https://github.com/binarylogic/authlogic_ldap +- Authlogic Facebook Connect: https://github.com/kalasjocke/authlogic-facebook-connect +- Authlogic Facebook Connect (New JS API): https://github.com/studybyte/authlogic_facebook_connect +- Authlogic Facebook Shim https://github.com/james2m/authlogic_facebook_shim +- Authlogic OAuth (Twitter): https://github.com/jrallison/authlogic_oauth +- Authlogic Oauth and OpenID: https://github.com/lancejpollard/authlogic-connect +- Authlogic PAM: https://github.com/nbudin/authlogic_pam +- Authlogic x509: https://github.com/auth-scc/authlogic_x509 + +If you create one of your own, please let us know about it so we can add it to +this list. Or just fork the project, add your link, and send us a pull request. + +## 6. Internals + +Interested in how all of this all works? Think about an ActiveRecord model. A +database connection must be established before you can use it. In the case of +Authlogic, a controller connection must be established before you can use it. It +uses that controller connection to modify cookies, the current session, login +with HTTP basic, etc. It connects to the controller through a before filter that +is automatically set in your controller which lets Authlogic know about the +current controller object. Then Authlogic leverages that to do everything, it's +a pretty simple design. Nothing crazy going on, Authlogic is just leveraging the +tools your framework provides in the controller object. + +## 7. Extending + +## 7.a. Extending UserSession + +Your `UserSession` is designed to be extended with callbacks. + +Example: Custom logging. + +``` +# user_session.rb +class UserSession < Authlogic::Session::Base + after_persisting :my_custom_logging + + private + + def my_custom_logging + Rails.logger.info( + format( + 'After authentication attempt, user id is %d', + record.send(record.class.primary_key) + ) + ) + end +end +``` + +To learn more about available callbacks, see the "Callbacks" documentation +in `authlogic/session/base.rb`. + +## 90. Compatibility + +| Version | branch | ruby | activerecord | +| ------- | ---------- | -------- | ------------- | +| 6.5.0 | 6-5-stable | >= 2.4.0 | >= 5.2, < 8.0 | +| 6.4.3 | 6-4-stable | >= 2.4.0 | >= 5.2, < 7.1 | +| 5.2 | 5-2-stable | >= 2.3.0 | >= 5.2, < 6.1 | +| 4.5 | 4-5-stable | >= 2.3.0 | >= 4.2, < 5.3 | +| 4.3 | 4-3-stable | >= 2.3.0 | >= 4.2, < 5.3 | +| 4.2 | 4-2-stable | >= 2.2.0 | >= 4.2, < 5.3 | +| 3 | 3-stable | >= 1.9.3 | >= 3.2, < 5.3 | +| 2 | rails2 | >= 1.9.3 | ~> 2.3.0 | +| 1 | ? | ? | ? | + +Under SemVer, [changes to dependencies][10] do not require a major release. + +## Intellectual Property + +Copyright (c) 2012 Ben Johnson of Binary Logic, released under the MIT license + +[1]: https://api.travis-ci.org/binarylogic/authlogic.svg?branch=master +[2]: https://travis-ci.org/binarylogic/authlogic +[3]: https://gemnasium.com/badges/github.com/binarylogic/authlogic.svg +[4]: https://gemnasium.com/binarylogic/authlogic +[5]: https://badge.fury.io/rb/authlogic.svg +[6]: http://badge.fury.io/rb/authlogic +[7]: https://codeclimate.com/github/binarylogic/authlogic.svg +[8]: https://codeclimate.com/github/binarylogic/authlogic +[9]: http://guides.rubyonrails.org/routing.html#resource-routing-the-rails-default +[10]: https://semver.org/spec/v2.0.0.html#what-should-i-do-if-i-update-my-own-dependencies-without-changing-the-public-api diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 574b4afc..00000000 --- a/README.rdoc +++ /dev/null @@ -1,246 +0,0 @@ -= Authlogic - -Authlogic is a clean, simple, and unobtrusive ruby authentication solution. - -A code example can replace a thousand words... - -Authlogic introduces a new type of model. You can have as many as you want, and name them whatever you want, just like your other models. In this example, we want to authenticate with the User model, which is inferred by the name: - - class UserSession < Authlogic::Session::Base - # specify configuration here, such as: - # logout_on_timeout true - # ...many more options in the documentation - end - -Log in with any of the following. Create a UserSessionsController and use it just like your other models: - - UserSession.create(:login => "bjohnson", :password => "my password", :remember_me => true) - session = UserSession.new(:login => "bjohnson", :password => "my password", :remember_me => true); session.save - UserSession.create(:openid_identifier => "identifier", :remember_me => true) # requires the authlogic-oid "add on" gem - UserSession.create(my_user_object, true) # skip authentication and log the user in directly, the true means "remember me" - -The above handles the entire authentication process for you. It first authenticates, then it sets up the proper session values and cookies to persist the session. Just like you would if you rolled your own authentication solution. - -You can also log out / destroy the session: - - session.destroy - -After a session has been created, you can persist it across requests. Thus keeping the user logged in: - - session = UserSession.find - -To get all of the nice authentication functionality in your model just do this: - - class User < ActiveRecord::Base - acts_as_authentic do |c| - c.my_config_option = my_value - end # the configuration block is optional - end - -This handles validations, etc. It is also "smart" in the sense that it if a login field is present it will use that to authenticate, if not it will look for an email field, etc. This is all configurable, but for 99% of cases that above is all you will need to do. - -Also, sessions are automatically maintained. You can switch this on and off with configuration, but the following will automatically log a user in after a successful registration: - - User.create(params[:user]) - -This also updates the session when the user changes his/her password. - -Authlogic is very flexible, it has a strong public API and a plethora of hooks to allow you to modify behavior and extend it. Check out the helpful links below to dig deeper. - -== Helpful links - -* Documentation: http://rdoc.info/projects/binarylogic/authlogic -* Repository: http://github.com/binarylogic/authlogic/tree/master -* Railscasts Screencast: http://railscasts.com/episodes/160-authlogic -* Live example with OpenID "add on": http://authlogicexample.binarylogic.com -* Live example repository with tutorial in README: http://github.com/binarylogic/authlogic_example/tree/master -* Tutorial: Reset passwords with Authlogic the RESTful way: http://www.binarylogic.com/2008/11/16/tutorial-reset-passwords-with-authlogic -* Issues: http://github.com/binarylogic/authlogic/issues -* Google group: http://groups.google.com/group/authlogic - -Before contacting me directly, please read: - -If you find a bug or a problem please post it in the issues section. If you need help with something, please use google groups. I check both regularly and get emails when anything happens, so that is the best place to get help. This also benefits other people in the future with the same questions / problems. Thank you. - -== Authlogic "add ons" - -* Authlogic OpenID addon: http://github.com/binarylogic/authlogic_openid -* Authlogic LDAP addon: http://github.com/binarylogic/authlogic_ldap -* Authlogic Facebook Connect: http://github.com/kalasjocke/authlogic_facebook_connect -* Authlogic OAuth (Twitter): http://github.com/jrallison/authlogic_oauth -* Authlogic PAM: http://github.com/nbudin/authlogic_pam - -If you create one of your own, please let me know about it so I can add it to this list. Or just fork the project, add your link, and send me a pull request. - -== Session bugs (please read if you are having issues with logging in / out) - -Apparently there is a bug with apache / passenger for v2.1.X with sessions not working properly. This is most likely your problem if you are having trouble logging in / out. This is *not* an Authlogic issue. This can be solved by updating passener or using an alternative session store solution, such as active record store. - -== Documentation explanation - -You can find anything you want about Authlogic in the {documentation}[http://rdoc.info/projects/binarylogic/authlogic], all that you need to do is understand the basic design behind it. - -That being said, there are 2 models involved during authentication. Your Authlogic model and your ActiveRecord model: - -1. Authlogic::Session, your session models that extend Authlogic::Session::Base. -2. Authlogic::ActsAsAuthentic, which adds in functionality to your ActiveRecord model when you call acts_as_authentic. - -Each of the above has its various sub modules that contain common logic. The sub modules are responsible for including *everything* related to it: configuration, class methods, instance methods, etc. - -For example, if you want to timeout users after a certain period of inactivity, you would look in Authlogic::Session::Timeout. To help you out, I listed the following publicly relevant modules with short descriptions. For the sake of brevity, there are more modules than listed here, the ones not listed are more for internal use, but you can easily read up on them in the {documentation}[http://rdoc.info/projects/binarylogic/authlogic]. - -=== Authlogic::ActsAsAuthentic sub modules - -These modules are for the ActiveRecord side of things, the models that call acts_as_authentic. - -* Authlogic::ActsAsAuthentic::Base - Provides the acts_as_authentic class method and includes all of the submodules. -* Authlogic::ActsAsAuthentic::Email - Handles everything related to the email field. -* Authlogic::ActsAsAuthentic::LoggedInStatus - Provides handy named scopes and methods for determining if the user is logged in or out. -* Authlogic::ActsAsAuthentic::Login - Handles everything related to the login field. -* Authlogic::ActsAsAuthentic::MagicColumns - Handles everything related to the "magic" fields: login_count, failed_login_count, last_request_at, etc. -* Authlogic::ActsAsAuthentic::Password - This one is important. It handles encrypting your password, salting it, etc. It also has support for transitioning password algorithms. -* Authlogic::ActsAsAuthentic::PerishableToken - Handles maintaining the perishable token field, also provides a class level method for finding record using the token. -* Authlogic::ActsAsAuthentic::PersistenceToken - Handles maintaining the persistence token. This is the token stored in cookies and sessions to persist the users session. -* Authlogic::ActsAsAuthentic::RestfulAuthentication - Provides configuration options to easily migrate from the restful_authentication plugin. -* Authlogic::ActsAsAuthentic::SessionMaintenance - Handles automatic session maintenance. EX: a new user registers, automatically log them in. Or a user changes their password, update their session. -* Authlogic::ActsAsAuthentic::SingleAccessToken - Handles maintaining the single access token. -* Authlogic::ActsAsAuthentic::ValidationsScope - Allows you to scope all validations, etc. Just like the :scope option for validates_uniqueness_of - -=== Authlogic::Session sub modules - -These modules are for the models that extend Authlogic::Session::Base. - -* Authlogic::Session::BruteForceProtection - Disables accounts after a certain number of consecutive failed logins attempted. -* Authlogic::Session::Callbacks - Your tools to extend, change, or add onto Authlogic. Lets you hook in and do just about anything you want. Start here if you want to write a plugin or add-on for Authlogic -* Authlogic::Session::Cookies - Authentication via cookies. -* Authlogic::Session::Existence - Creating, saving, and destroying objects. -* Authlogic::Session::HttpAuth - Authentication via basic HTTP authentication. -* Authlogic::Session::Id - Allows sessions to be separated by an id, letting you have multiple sessions for a single user. -* Authlogic::Session::MagicColumns - Maintains "magic" database columns, similar to created_at and updated_at for ActiveRecord. -* Authlogic::Session::MagicStates - Automatically validates based on the records states: active?, approved?, and confirmed?. If those methods exist for the record. -* Authlogic::Session::Params - Authentication via params, aka single access token. -* Authlogic::Session::Password - Authentication via a traditional username and password. -* Authlogic::Session::Persistence - Persisting sessions / finding sessions. -* Authlogic::Session::Session - Authentication via the session, the controller session that is. -* Authlogic::Session::Timeout - Automatically logging out after a certain period of inactivity. -* Authlogic::Session::UnauthorizedRecord - Handles authentication by passing an ActiveRecord object directly. -* Authlogic::Session::Validation - Validation / errors. - -=== Miscellaneous modules - -Miscellaneous modules that shared across the authentication process and are more "utility" modules and classes. - -* Authlogic::AuthenticatesMany - Responsible for allowing you to scope sessions to a parent record. Similar to a has_many and belongs_to relationship. This lets you do the same thing with sessions. -* Authlogic::CryptoProviders - Contains various encryption algorithms that Authlogic uses, allowing you to choose your encryption method. -* Authlogic::I18n - Acts JUST LIKE the rails I18n library, and provides internationalization to Authlogic. -* Authlogic::Random - A simple class to generate random tokens. -* Authlogic::Regex - Contains regular expressions used in Authlogic. Such as those to validate the format of the log or email. -* Authlogic::TestCase - Various helper methods for testing frameworks to help you test your code. -* Authlogic::Version - A handy class for determine the version of Authlogic in a number of ways. - -== Quick Rails example - -What if creating sessions worked like an ORM library on the surface... - - UserSession.create(params[:user_session]) - -What if your user sessions controller could look just like your other controllers... - - class UserSessionsController < ApplicationController - def new - @user_session = UserSession.new - end - - def create - @user_session = UserSession.new(params[:user_session]) - if @user_session.save - redirect_to account_url - else - render :action => :new - end - end - - def destroy - current_user_session.destroy - redirect_to new_user_session_url - end - end - -As you can see, this fits nicely into the RESTful development pattern. What about the view... - - <% form_for @user_session do |f| %> - <%= f.error_messages %> - <%= f.label :login %>
- <%= f.text_field :login %>
-
- <%= f.label :password %>
- <%= f.password_field :password %>
-
- <%= f.submit "Login" %> - <% end %> - -Or how about persisting the session... - - class ApplicationController - helper_method :current_user_session, :current_user - - private - def current_user_session - return @current_user_session if defined?(@current_user_session) - @current_user_session = UserSession.find - end - - def current_user - return @current_user if defined?(@current_user) - @current_user = current_user_session && current_user_session.user - end - end - -== Install & Use - -Install the gem / plugin (recommended) - -From rubyforge: - - $ sudo gem install authlogic - -Or from github: - - $ sudo gem install binarylogic-authlogic - -Now just add the gem dependency in your projects configuration. - -Or you can install this as a plugin: - - script/plugin install git://github.com/binarylogic/authlogic.git - -== Detailed Setup Tutorial - -See the {authlogic example}[http://github.com/binarylogic/authlogic_example/tree/master] for a detailed setup tutorial. I did this because not only do you have a tutorial to go by, but you have an example app that uses the same tutorial, so you can play around with with the code. If you have problems you can compare the code to see what you are doing differently. - -== Testing - -I think one of the best aspects of Authlogic is testing. For one, it cuts out a lot of redundant tests in your applications because Authlogic is already thoroughly tested for you. It doesn't include a bunch of tests into your application, because it comes tested, just like any other library. - -For example, think about ActiveRecord. You don't test the internals of ActiveRecord, because the creators of ActiveRecord have already tested the internals for you. It wouldn't make sense for ActiveRecord to copy it's hundreds of tests into your applications. The same concept applies to Authlogic. You only need to test code you write that is specific to your application, just like everything else in your application. - -That being said, testing your code that uses Authlogic is easy. Since everyone uses different testing suites, I created a helpful module called Authlogic::TestCase, which is basically a set of tools for testing code using Authlogic. I explain testing Authlogic thoroughly in the {Authlogic::TestCase section of the documentation}[http://rdoc.info/rdoc/binarylogic/authlogic/blob/f2f6988d3b97e11770b00b72a7a9733df69ffa5b/Authlogic/TestCase.html]. It should answer any questions you have in regards to testing Authlogic. - -== Tell me quickly how Authlogic works - -Interested in how all of this all works? Think about an ActiveRecord model. A database connection must be established before you can use it. In the case of Authlogic, a controller connection must be established before you can use it. It uses that controller connection to modify cookies, the current session, login with HTTP basic, etc. It connects to the controller through a before filter that is automatically set in your controller which lets Authlogic know about the current controller object. Then Authlogic leverages that to do everything, it's a pretty simple design. Nothing crazy going on, Authlogic is just leveraging the tools your framework provides in the controller object. - -== What sets Authlogic apart and why I created it - -What inspired me to create Authlogic was the messiness of the current authentication solutions. Put simply, they just didn't feel right, because the logic was not organized properly. As you may know, a common misconception with the MVC design pattern is that the model "M" is only for data access logic, which is wrong. A model is a place for domain logic. This is why the RESTful design pattern and the current authentication solutions don't play nice. Authlogic solves this by placing the session maintenance logic into its own domain (aka "model"). Moving session maintenance into its own domain has its benefits: - -1. It's cleaner. There are no generators in Authlogic. Authlogic provides a class that you can use, it's plain and simple ruby. More importantly, the code in your app is code you write, written the way you want, nice and clean. It's code that should be in your app and is specific to your app, not a redundant authentication pattern. -2. Easier to stay up-to-date. To make my point, take a look at the commits to any other authentication solution, then look at the {commits for authlogic}[http://github.com/binarylogic/authlogic/commits/master]. How many commits could you easily start using if you already had an app using that solution? With an alternate solution, very few, if any. All of those cool new features and bug fixes are going to have be manually added or wait for your next application. Which is the main reason a generator is not suitable as an authentication solution. With Authlogic you can start using the latest code with a simple update of a gem. No generators, no mess. -3. It ties everything together on the domain level. Take a new user registration for example, no reason to manually log the user in, authlogic handles this for you via callbacks. The same applies to a user changing their password. Authlogic handles maintaining the session for you. -4. No redundant tests. Because Authlogic doesn't use generators, #1 also applies to tests. Authlogic is *thoroughly* tested for you. You don't go and test the internals of ActiveRecord in each of your apps do you? So why do the same for Authlogic? Your application tests should be for application specific code. Get rid of the noise and make your tests focused and concise, no reason to copy tests from app to app. -5. Framework agnostic. Authlogic can be used in *any* ruby framework you want: Rails, Merb, Sinatra, Mack, your own framework, whatever. It's not tied down to Rails. It does this by abstracting itself from these framework's controllers by using a controller adapter. Thanks to {Rack}[http://rack.rubyforge.org/], there is a defined standard for controller structure, and that's what Authlogic's abstract adapter follows. So if your controller follows the rack standards, you don't need to do anything. Any place it deviates from this is solved by a simple adapter for your framework that closes these gaps. For an example, checkout the Authlogic::ControllerAdapters::MerbAdapter. -5. You are not restricted to a single session. Think about Apple's me.com, where they need you to authenticate a second time before changing your billing information. Why not just create a second session for this? It works just like your initial session. Then your billing controller can require an "ultra secure" session. -6. Easily extendable. One of the distinct advantages of using a library is the ability to use its API, assuming it has one. Authlogic has an *excellent* public API, meaning it can easily be extended and grow beyond the core library. Checkout the "add ons" list above to see what I mean. - - -Copyright (c) 2009 {Ben Johnson of Binary Logic}[http://www.binarylogic.com], released under the MIT license diff --git a/Rakefile b/Rakefile index b6e53165..5828c10d 100644 --- a/Rakefile +++ b/Rakefile @@ -1,41 +1,24 @@ -require 'rubygems' -require 'rake' +# frozen_string_literal: true -begin - require 'jeweler' - Jeweler::Tasks.new do |gem| - gem.name = "authlogic" - gem.summary = "A clean, simple, and unobtrusive ruby authentication solution." - gem.email = "bjohnson@binarylogic.com" - gem.homepage = "http://github.com/binarylogic/authlogic" - gem.authors = ["Ben Johnson of Binary Logic"] - gem.add_dependency "activesupport" - end - Jeweler::GemcutterTasks.new -rescue LoadError - puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" -end +require "rubygems" +require "bundler" + +Bundler.setup -require 'rake/testtask' +require "rake/testtask" Rake::TestTask.new(:test) do |test| - test.libs << 'test' - test.pattern = 'test/**/*_test.rb' - test.verbose = true -end + test.libs << "test" + test.pattern = "test/**/*_test.rb" + test.verbose = false -begin - require 'rcov/rcovtask' - Rcov::RcovTask.new do |test| - test.libs << 'test' - test.pattern = 'test/**/*_test.rb' - test.verbose = true - end -rescue LoadError - task :rcov do - abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" - end + # Set interpreter warning level to 2 (verbose) + test.ruby_opts << "-W2" end -task :test => :check_dependencies +require "rubocop/rake_task" +RuboCop::RakeTask.new + +task default: %i[rubocop test] -task :default => :test +require "coveralls/rake/task" +Coveralls::RakeTask.new diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..dac1225c --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,22 @@ +# Upgrading Authlogic + +Supplemental instructions to complement CHANGELOG.md. + +## 3.4.0 + +In version 3.4.0, released 2014-03-03, the default crypto_provider was changed +from *Sha512* to *SCrypt*. + +If you never set a crypto_provider and are upgrading, your passwords will break +unless you specify `Sha512`. + +``` ruby +c.crypto_provider = Authlogic::CryptoProviders::Sha512 +``` + +And if you want to automatically upgrade from *Sha512* to *SCrypt* as users login: + +```ruby +c.transition_from_crypto_providers = [Authlogic::CryptoProviders::Sha512] +c.crypto_provider = Authlogic::CryptoProviders::SCrypt +``` diff --git a/VERSION.yml b/VERSION.yml deleted file mode 100644 index 7f597e79..00000000 --- a/VERSION.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -:major: 2 -:minor: 1 -:patch: 6 -:build: diff --git a/authlogic.gemspec b/authlogic.gemspec index 0f2d24cc..407d9194 100644 --- a/authlogic.gemspec +++ b/authlogic.gemspec @@ -1,216 +1,56 @@ -# Generated by jeweler -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command -# -*- encoding: utf-8 -*- +# frozen_string_literal: true -Gem::Specification.new do |s| - s.name = %q{authlogic} - s.version = "2.1.6" +require "English" - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Ben Johnson of Binary Logic"] - s.date = %q{2010-08-04} - s.email = %q{bjohnson@binarylogic.com} - s.extra_rdoc_files = [ - "LICENSE", - "README.rdoc" - ] - s.files = [ - ".gitignore", - "CHANGELOG.rdoc", - "LICENSE", - "README.rdoc", - "Rakefile", - "VERSION.yml", - "authlogic.gemspec", - "generators/session/session_generator.rb", - "generators/session/templates/session.rb", - "init.rb", - "lib/authlogic.rb", - "lib/authlogic/acts_as_authentic/base.rb", - "lib/authlogic/acts_as_authentic/email.rb", - "lib/authlogic/acts_as_authentic/logged_in_status.rb", - "lib/authlogic/acts_as_authentic/login.rb", - "lib/authlogic/acts_as_authentic/magic_columns.rb", - "lib/authlogic/acts_as_authentic/password.rb", - "lib/authlogic/acts_as_authentic/perishable_token.rb", - "lib/authlogic/acts_as_authentic/persistence_token.rb", - "lib/authlogic/acts_as_authentic/restful_authentication.rb", - "lib/authlogic/acts_as_authentic/session_maintenance.rb", - "lib/authlogic/acts_as_authentic/single_access_token.rb", - "lib/authlogic/acts_as_authentic/validations_scope.rb", - "lib/authlogic/authenticates_many/association.rb", - "lib/authlogic/authenticates_many/base.rb", - "lib/authlogic/controller_adapters/abstract_adapter.rb", - "lib/authlogic/controller_adapters/merb_adapter.rb", - "lib/authlogic/controller_adapters/rails_adapter.rb", - "lib/authlogic/controller_adapters/sinatra_adapter.rb", - "lib/authlogic/crypto_providers/aes256.rb", - "lib/authlogic/crypto_providers/bcrypt.rb", - "lib/authlogic/crypto_providers/md5.rb", - "lib/authlogic/crypto_providers/sha1.rb", - "lib/authlogic/crypto_providers/sha256.rb", - "lib/authlogic/crypto_providers/sha512.rb", - "lib/authlogic/crypto_providers/wordpress.rb", - "lib/authlogic/i18n.rb", - "lib/authlogic/i18n/translator.rb", - "lib/authlogic/random.rb", - "lib/authlogic/regex.rb", - "lib/authlogic/session/activation.rb", - "lib/authlogic/session/active_record_trickery.rb", - "lib/authlogic/session/base.rb", - "lib/authlogic/session/brute_force_protection.rb", - "lib/authlogic/session/callbacks.rb", - "lib/authlogic/session/cookies.rb", - "lib/authlogic/session/existence.rb", - "lib/authlogic/session/foundation.rb", - "lib/authlogic/session/http_auth.rb", - "lib/authlogic/session/id.rb", - "lib/authlogic/session/klass.rb", - "lib/authlogic/session/magic_columns.rb", - "lib/authlogic/session/magic_states.rb", - "lib/authlogic/session/params.rb", - "lib/authlogic/session/password.rb", - "lib/authlogic/session/perishable_token.rb", - "lib/authlogic/session/persistence.rb", - "lib/authlogic/session/priority_record.rb", - "lib/authlogic/session/scopes.rb", - "lib/authlogic/session/session.rb", - "lib/authlogic/session/timeout.rb", - "lib/authlogic/session/unauthorized_record.rb", - "lib/authlogic/session/validation.rb", - "lib/authlogic/test_case.rb", - "lib/authlogic/test_case/mock_controller.rb", - "lib/authlogic/test_case/mock_cookie_jar.rb", - "lib/authlogic/test_case/mock_logger.rb", - "lib/authlogic/test_case/mock_request.rb", - "lib/authlogic/test_case/rails_request_adapter.rb", - "rails/init.rb", - "shoulda_macros/authlogic.rb", - "test/acts_as_authentic_test/base_test.rb", - "test/acts_as_authentic_test/email_test.rb", - "test/acts_as_authentic_test/logged_in_status_test.rb", - "test/acts_as_authentic_test/login_test.rb", - "test/acts_as_authentic_test/magic_columns_test.rb", - "test/acts_as_authentic_test/password_test.rb", - "test/acts_as_authentic_test/perishable_token_test.rb", - "test/acts_as_authentic_test/persistence_token_test.rb", - "test/acts_as_authentic_test/restful_authentication_test.rb", - "test/acts_as_authentic_test/session_maintenance_test.rb", - "test/acts_as_authentic_test/single_access_test.rb", - "test/authenticates_many_test.rb", - "test/crypto_provider_test/aes256_test.rb", - "test/crypto_provider_test/bcrypt_test.rb", - "test/crypto_provider_test/sha1_test.rb", - "test/crypto_provider_test/sha256_test.rb", - "test/crypto_provider_test/sha512_test.rb", - "test/fixtures/companies.yml", - "test/fixtures/employees.yml", - "test/fixtures/projects.yml", - "test/fixtures/users.yml", - "test/i18n_test.rb", - "test/libs/affiliate.rb", - "test/libs/company.rb", - "test/libs/employee.rb", - "test/libs/employee_session.rb", - "test/libs/ldaper.rb", - "test/libs/ordered_hash.rb", - "test/libs/project.rb", - "test/libs/user.rb", - "test/libs/user_session.rb", - "test/random_test.rb", - "test/session_test/activation_test.rb", - "test/session_test/active_record_trickery_test.rb", - "test/session_test/brute_force_protection_test.rb", - "test/session_test/callbacks_test.rb", - "test/session_test/cookies_test.rb", - "test/session_test/credentials_test.rb", - "test/session_test/existence_test.rb", - "test/session_test/http_auth_test.rb", - "test/session_test/id_test.rb", - "test/session_test/klass_test.rb", - "test/session_test/magic_columns_test.rb", - "test/session_test/magic_states_test.rb", - "test/session_test/params_test.rb", - "test/session_test/password_test.rb", - "test/session_test/perishability_test.rb", - "test/session_test/persistence_test.rb", - "test/session_test/scopes_test.rb", - "test/session_test/session_test.rb", - "test/session_test/timeout_test.rb", - "test/session_test/unauthorized_record_test.rb", - "test/session_test/validation_test.rb", - "test/test_helper.rb" +require_relative "lib/authlogic/version" + +::Gem::Specification.new do |s| + s.name = "authlogic" + s.version = ::Authlogic.gem_version.to_s + s.platform = ::Gem::Platform::RUBY + s.authors = [ + "Ben Johnson", + "Tieg Zaharia", + "Jared Beck" ] - s.homepage = %q{http://github.com/binarylogic/authlogic} - s.rdoc_options = ["--charset=UTF-8"] - s.require_paths = ["lib"] - s.rubygems_version = %q{1.3.7} - s.summary = %q{A clean, simple, and unobtrusive ruby authentication solution.} - s.test_files = [ - "test/acts_as_authentic_test/base_test.rb", - "test/acts_as_authentic_test/email_test.rb", - "test/acts_as_authentic_test/logged_in_status_test.rb", - "test/acts_as_authentic_test/login_test.rb", - "test/acts_as_authentic_test/magic_columns_test.rb", - "test/acts_as_authentic_test/password_test.rb", - "test/acts_as_authentic_test/perishable_token_test.rb", - "test/acts_as_authentic_test/persistence_token_test.rb", - "test/acts_as_authentic_test/restful_authentication_test.rb", - "test/acts_as_authentic_test/session_maintenance_test.rb", - "test/acts_as_authentic_test/single_access_test.rb", - "test/authenticates_many_test.rb", - "test/crypto_provider_test/aes256_test.rb", - "test/crypto_provider_test/bcrypt_test.rb", - "test/crypto_provider_test/sha1_test.rb", - "test/crypto_provider_test/sha256_test.rb", - "test/crypto_provider_test/sha512_test.rb", - "test/i18n_test.rb", - "test/libs/affiliate.rb", - "test/libs/company.rb", - "test/libs/employee.rb", - "test/libs/employee_session.rb", - "test/libs/ldaper.rb", - "test/libs/ordered_hash.rb", - "test/libs/project.rb", - "test/libs/user.rb", - "test/libs/user_session.rb", - "test/random_test.rb", - "test/session_test/activation_test.rb", - "test/session_test/active_record_trickery_test.rb", - "test/session_test/brute_force_protection_test.rb", - "test/session_test/callbacks_test.rb", - "test/session_test/cookies_test.rb", - "test/session_test/credentials_test.rb", - "test/session_test/existence_test.rb", - "test/session_test/http_auth_test.rb", - "test/session_test/id_test.rb", - "test/session_test/klass_test.rb", - "test/session_test/magic_columns_test.rb", - "test/session_test/magic_states_test.rb", - "test/session_test/params_test.rb", - "test/session_test/password_test.rb", - "test/session_test/perishability_test.rb", - "test/session_test/persistence_test.rb", - "test/session_test/scopes_test.rb", - "test/session_test/session_test.rb", - "test/session_test/timeout_test.rb", - "test/session_test/unauthorized_record_test.rb", - "test/session_test/validation_test.rb", - "test/test_helper.rb" + s.email = [ + "bjohnson@binarylogic.com", + "tieg.zaharia@gmail.com", + "jared@jaredbeck.com" ] + s.homepage = "https://github.com/binarylogic/authlogic" + s.summary = "An unobtrusive ruby authentication library based on ActiveRecord." + s.license = "MIT" + s.metadata = { "rubygems_mfa_required" => "true" } + s.required_ruby_version = ">= 2.6.0" - if s.respond_to? :specification_version then - current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION - s.specification_version = 3 + # See doc/rails_support_in_authlogic_5.0.md + s.add_dependency "activemodel", [">= 5.2", "< 8.1"] + s.add_dependency "activerecord", [">= 5.2", "< 8.1"] + s.add_dependency "activesupport", [">= 5.2", "< 8.1"] + s.add_dependency "request_store", "~> 1.0" + s.add_development_dependency "bcrypt", "~> 3.1" + s.add_development_dependency "byebug", "~> 11.1.3" + s.add_development_dependency "coveralls_reborn", "~> 0.28.0" + s.add_development_dependency "minitest", "< 5.19.0" # See https://github.com/binarylogic/authlogic/issues/766 + s.add_development_dependency "minitest-reporters", "~> 1.3" + s.add_development_dependency "mutex_m", "~> 0.3.0" + s.add_development_dependency "rake", "~> 13.0" + s.add_development_dependency "rubocop", "~> 0.80.1" + s.add_development_dependency "rubocop-performance", "~> 1.1" + s.add_development_dependency "scrypt", ">= 1.2", "< 4.0" + s.add_development_dependency "simplecov", "~> 0.22.0" + s.add_development_dependency "simplecov-console", "~> 0.9.1" + s.add_development_dependency "timecop", "~> 0.7" - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, [">= 0"]) - else - s.add_dependency(%q, [">= 0"]) - end - else - s.add_dependency(%q, [">= 0"]) - end + # To reduce gem size, only the minimum files are included. + # + # Tests are intentionally excluded. We only support our own test suite, we do + # not have enough volunteers to support "in-situ" testing. + s.files = `git ls-files -z`.split("\x0").select { |f| + f.match(%r{^(LICENSE|lib|authlogic.gemspec)/}) + } + s.test_files = [] # not packaged, see above + s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } + s.require_paths = ["lib"] end - diff --git a/doc/rails_support_in_authlogic_5.0.md b/doc/rails_support_in_authlogic_5.0.md new file mode 100644 index 00000000..c5b7af34 --- /dev/null +++ b/doc/rails_support_in_authlogic_5.0.md @@ -0,0 +1,14 @@ +# Rails support in Authlogic 5 + +2018-12-03 + +Authlogic 5 adds support for rails 6.0, and drops support for rails < 5.2. +This is in keeping with the [Maintenance Policy +for Ruby on Rails](https://guides.rubyonrails.org/maintenance_policy.html). +When rails 6 is released, rails < 5.2 will be EoL. Authlogic 5 reflects this. + +Authlogic 4 will continue to support rails `>= 4.2, < 5.3`. We'll continue to +accept contributions to Authlogic 4 for as long as is practical. + +In conclusion, Authlogic 4 supports the "rails 5 era" and Authlogic 5 supports +the "rails 6 era". diff --git a/doc/use_normal_rails_validation.md b/doc/use_normal_rails_validation.md new file mode 100644 index 00000000..3af0d485 --- /dev/null +++ b/doc/use_normal_rails_validation.md @@ -0,0 +1,94 @@ +# Use Normal ActiveRecord Validation + +In Authlogic 4.4.0, [we deprecated][1] the features of Authlogic related to +validating email, login, and password. In 5.0.0 these features will be dropped. +Use normal ActiveRecord validations instead. + +## Instructions for 4.4.0 + +First, disable the deprecated Authlogic validations: + + acts_as_authentic do |c| + c.validate_email_field = false + c.validate_login_field = false + c.validate_password_field = false + end + +Then, use normal ActiveRecord validations instead. For example, instead of +the Authlogic method validates_length_of_email_field_options, use + + validates :email, length: { ... } + +It might be a good idea to replace these one field at a time, ie. email, +then login, then password; one field per commit. + +Finish this process before upgrading to Authlogic 5. + +## Default Values + +The following validations represent the defaults in Authlogic 4. Merge these +defaults with any settings you may have overwritten. + +```ruby +EMAIL = / + \A + [A-Z0-9_.&%+\-']+ # mailbox + @ + (?:[A-Z0-9\-]+\.)+ # subdomains + (?:[A-Z]{2,25}) # TLD + \z +/ix +LOGIN = /\A[a-zA-Z0-9_][a-zA-Z0-9\.+\-_@ ]+\z/ + +validates :email, + format: { + with: EMAIL, + message: proc { + ::Authlogic::I18n.t( + "error_messages.email_invalid", + default: "should look like an email address." + ) + } + }, + length: { maximum: 100 }, + uniqueness: { + case_sensitive: false, + if: :will_save_change_to_email? + } + +validates :login, + format: { + with: LOGIN, + message: proc { + ::Authlogic::I18n.t( + "error_messages.login_invalid", + default: "should use only letters, numbers, spaces, and .-_@+ please." + ) + } + }, + length: { within: 3..100 }, + uniqueness: { + case_sensitive: false, + if: :will_save_change_to_login? + } + +validates :password, + confirmation: { if: :require_password? }, + length: { + minimum: 8, + if: :require_password? + } +validates :password_confirmation, + length: { + minimum: 8, + if: :require_password? +} +``` + +## Motivation + +The deprecated features save people some time in the beginning, when setting up +Authlogic. But, later in the life of a project, when these settings need to +change, it is obscure compared to normal ActiveRecord validations. + +[1]: https://github.com/binarylogic/authlogic/pull/623 diff --git a/gemfiles/rails_5.2.rb b/gemfiles/rails_5.2.rb new file mode 100644 index 00000000..4840c6f5 --- /dev/null +++ b/gemfiles/rails_5.2.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 5.2.0" +gem "activesupport", "~> 5.2.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 1.4.0" diff --git a/gemfiles/rails_6.0.rb b/gemfiles/rails_6.0.rb new file mode 100644 index 00000000..69147ebe --- /dev/null +++ b/gemfiles/rails_6.0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 6.0.0" +gem "activesupport", "~> 6.0.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 1.4.0" diff --git a/gemfiles/rails_6.1.rb b/gemfiles/rails_6.1.rb new file mode 100644 index 00000000..ed03ee5b --- /dev/null +++ b/gemfiles/rails_6.1.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 6.1.0" +gem "activesupport", "~> 6.1.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 1.4.0" diff --git a/gemfiles/rails_7.0.rb b/gemfiles/rails_7.0.rb new file mode 100644 index 00000000..1fdddbbc --- /dev/null +++ b/gemfiles/rails_7.0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 7.0.0" +gem "activesupport", "~> 7.0.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 1.6.0" diff --git a/gemfiles/rails_7.1.rb b/gemfiles/rails_7.1.rb new file mode 100644 index 00000000..fecba797 --- /dev/null +++ b/gemfiles/rails_7.1.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 7.1.0" +gem "activesupport", "~> 7.1.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 1.6.0" diff --git a/gemfiles/rails_7.2.rb b/gemfiles/rails_7.2.rb new file mode 100644 index 00000000..8904a020 --- /dev/null +++ b/gemfiles/rails_7.2.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 7.2.0" +gem "activesupport", "~> 7.2.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 2.0.0" diff --git a/gemfiles/rails_8.0.rb b/gemfiles/rails_8.0.rb new file mode 100644 index 00000000..f7270f39 --- /dev/null +++ b/gemfiles/rails_8.0.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +source "https://rubygems.org" +gemspec path: ".." + +gem "activerecord", "~> 8.0.0" +gem "activesupport", "~> 8.0.0" +gem "mysql2", "~> 0.5.6" +gem "pg", "~> 1.5.8" +gem "sqlite3", "~> 2.1.0" diff --git a/generators/session/session_generator.rb b/generators/session/session_generator.rb deleted file mode 100644 index 6b2f3cc1..00000000 --- a/generators/session/session_generator.rb +++ /dev/null @@ -1,9 +0,0 @@ -class SessionGenerator < Rails::Generator::NamedBase - def manifest - record do |m| - m.class_collisions class_name - m.directory File.join('app/models', class_path) - m.template 'session.rb', File.join('app/models', class_path, "#{file_name}.rb") - end - end -end diff --git a/generators/session/templates/session.rb b/generators/session/templates/session.rb deleted file mode 100644 index c42df984..00000000 --- a/generators/session/templates/session.rb +++ /dev/null @@ -1,2 +0,0 @@ -class <%= class_name %> < Authlogic::Session::Base -end \ No newline at end of file diff --git a/init.rb b/init.rb deleted file mode 100644 index 09d8ec6f..00000000 --- a/init.rb +++ /dev/null @@ -1 +0,0 @@ -require File.dirname(__FILE__) + "/rails/init.rb" \ No newline at end of file diff --git a/lib/authlogic.rb b/lib/authlogic.rb index 3efd3d63..d3a18de1 100644 --- a/lib/authlogic.rb +++ b/lib/authlogic.rb @@ -1,64 +1,36 @@ -require "active_record" +# frozen_string_literal: true -AUTHLOGIC_PATH = File.dirname(__FILE__) + "/authlogic/" +require_relative "authlogic/errors" +require_relative "authlogic/i18n" +require_relative "authlogic/random" +require_relative "authlogic/config" -[ - "i18n", - "random", - "regex", - - "controller_adapters/abstract_adapter", - - "crypto_providers/md5", - "crypto_providers/sha1", - "crypto_providers/sha256", - "crypto_providers/sha512", - "crypto_providers/bcrypt", - "crypto_providers/aes256", - - "authenticates_many/base", - "authenticates_many/association", - - "acts_as_authentic/email", - "acts_as_authentic/logged_in_status", - "acts_as_authentic/login", - "acts_as_authentic/magic_columns", - "acts_as_authentic/password", - "acts_as_authentic/perishable_token", - "acts_as_authentic/persistence_token", - "acts_as_authentic/restful_authentication", - "acts_as_authentic/session_maintenance", - "acts_as_authentic/single_access_token", - "acts_as_authentic/validations_scope", - "acts_as_authentic/base", - - "session/activation", - "session/active_record_trickery", - "session/brute_force_protection", - "session/callbacks", - "session/cookies", - "session/existence", - "session/foundation", - "session/http_auth", - "session/id", - "session/klass", - "session/magic_columns", - "session/magic_states", - "session/params", - "session/password", - "session/perishable_token", - "session/persistence", - "session/priority_record", - "session/scopes", - "session/session", - "session/timeout", - "session/unauthorized_record", - "session/validation", - "session/base" -].each do |library| - require AUTHLOGIC_PATH + library - end +require_relative "authlogic/controller_adapters/abstract_adapter" +require_relative "authlogic/cookie_credentials" -require AUTHLOGIC_PATH + "controller_adapters/rails_adapter" if defined?( Rails ) -require AUTHLOGIC_PATH + "controller_adapters/merb_adapter" if defined?( Merb ) -require AUTHLOGIC_PATH + "controller_adapters/sinatra_adapter" if defined?( Sinatra ) +require_relative "authlogic/crypto_providers" + +require_relative "authlogic/acts_as_authentic/email" +require_relative "authlogic/acts_as_authentic/logged_in_status" +require_relative "authlogic/acts_as_authentic/login" +require_relative "authlogic/acts_as_authentic/magic_columns" +require_relative "authlogic/acts_as_authentic/password" +require_relative "authlogic/acts_as_authentic/perishable_token" +require_relative "authlogic/acts_as_authentic/persistence_token" +require_relative "authlogic/acts_as_authentic/session_maintenance" +require_relative "authlogic/acts_as_authentic/single_access_token" +require_relative "authlogic/acts_as_authentic/base" + +require_relative "authlogic/session/magic_column/assigns_last_request_at" +require_relative "authlogic/session/base" + +# Authlogic uses ActiveSupport's core extensions like `strip_heredoc` and +# `squish`. ActiveRecord does not `require` these exensions, so we must. +# +# It's possible that we could save a few milliseconds by loading only the +# specific core extensions we need, but `all.rb` is simpler. We can revisit this +# decision if it becomes a problem. +require "active_support/all" + +require_relative "authlogic/controller_adapters/rails_adapter" if defined?(Rails) +require_relative "authlogic/controller_adapters/sinatra_adapter" if defined?(Sinatra) diff --git a/lib/authlogic/acts_as_authentic/base.rb b/lib/authlogic/acts_as_authentic/base.rb index 3c4e10d0..e58fd327 100644 --- a/lib/authlogic/acts_as_authentic/base.rb +++ b/lib/authlogic/acts_as_authentic/base.rb @@ -1,18 +1,25 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic # Provides the base functionality for acts_as_authentic module Base def self.included(klass) klass.class_eval do + class_attribute :acts_as_authentic_modules + self.acts_as_authentic_modules ||= [] + extend Authlogic::Config extend Config end end - + + # The primary configuration of a model (often, `User`) for use with + # authlogic. These methods become class methods of ::ActiveRecord::Base. module Config - # This includes a lot of helpful methods for authenticating records which The Authlogic::Session module relies on. - # To use it just do: + # This includes a lot of helpful methods for authenticating records + # which the Authlogic::Session module relies on. To use it just do: # - # class User < ActiveRecord::Base + # class User < ApplicationRecord # acts_as_authentic # end # @@ -23,25 +30,23 @@ module Config # end # # See the various sub modules for the configuration they provide. - def acts_as_authentic(unsupported_options = nil, &block) - # Stop all configuration if the DB is not set up - return if !db_setup? - - raise ArgumentError.new("You are using the old v1.X.X configuration method for Authlogic. Instead of " + - "passing a hash of configuration options to acts_as_authentic, pass a block: acts_as_authentic { |c| c.my_option = my_value }") if !unsupported_options.nil? - + def acts_as_authentic yield self if block_given? + return unless db_setup? acts_as_authentic_modules.each { |mod| include mod } end - - # Since this part of Authlogic deals with another class, ActiveRecord, we can't just start including things - # in ActiveRecord itself. A lot of these module includes need to be triggered by the acts_as_authentic method - # call. For example, you don't want to start adding in email validations and what not into a model that has - # nothing to do with Authlogic. + + # Since this part of Authlogic deals with another class, ActiveRecord, + # we can't just start including things in ActiveRecord itself. A lot of + # these module includes need to be triggered by the acts_as_authentic + # method call. For example, you don't want to start adding in email + # validations and what not into a model that has nothing to do with + # Authlogic. # - # That being said, this is your tool for extending Authlogic and "hooking" into the acts_as_authentic call. + # That being said, this is your tool for extending Authlogic and + # "hooking" into the acts_as_authentic call. def add_acts_as_authentic_module(mod, action = :append) - modules = acts_as_authentic_modules + modules = acts_as_authentic_modules.clone case action when :append modules << mod @@ -49,59 +54,65 @@ def add_acts_as_authentic_module(mod, action = :append) modules = [mod] + modules end modules.uniq! - write_inheritable_attribute(:acts_as_authentic_modules, modules) + self.acts_as_authentic_modules = modules end - - # This is the same as add_acts_as_authentic_module, except that it removes the module from the list. + + # This is the same as add_acts_as_authentic_module, except that it + # removes the module from the list. def remove_acts_as_authentic_module(mod) - acts_as_authentic_modules.delete(mod) - acts_as_authentic_modules + modules = acts_as_authentic_modules.clone + modules.delete(mod) + self.acts_as_authentic_modules = modules + end + + # Some Authlogic modules requires a database connection with a existing + # users table by the moment when you call the `acts_as_authentic` + # method. If you try to call `acts_as_authentic` without a database + # connection, it will raise a `Authlogic::ModelSetupError`. + # + # If you rely on the User model before the database is setup correctly, + # set this field to false. + # * Default: false + # * Accepts: Boolean + def raise_on_model_setup_error(value = nil) + rw_config(:raise_on_model_setup_error, value, false) end - + alias raise_on_model_setup_error= raise_on_model_setup_error + private - def acts_as_authentic_modules - key = :acts_as_authentic_modules - inheritable_attributes.include?(key) ? read_inheritable_attribute(key) : [] - end - - def db_setup? - begin - column_names - true - rescue Exception - false - end - end - - def rw_config(key, value, default_value = nil, read_value = nil) - if value == read_value - inheritable_attributes.include?(key) ? read_inheritable_attribute(key) : default_value - else - write_inheritable_attribute(key, value) - end - end - - def first_column_to_exist(*columns_to_check) - if db_setup? - columns_to_check.each { |column_name| return column_name.to_sym if column_names.include?(column_name.to_s) } + + def db_setup? + column_names + true + rescue StandardError + raise ModelSetupError if raise_on_model_setup_error + false + end + + def first_column_to_exist(*columns_to_check) + if db_setup? + columns_to_check.each do |column_name| + if column_names.include?(column_name.to_s) + return column_name.to_sym + end end - columns_to_check.first && columns_to_check.first.to_sym end + columns_to_check.first&.to_sym + end end end end end -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::RestfulAuthentication -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken -::ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::ValidationsScope - +ActiveSupport.on_load :active_record do + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance + ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken +end diff --git a/lib/authlogic/acts_as_authentic/email.rb b/lib/authlogic/acts_as_authentic/email.rb index 4228acdd..2ed27811 100644 --- a/lib/authlogic/acts_as_authentic/email.rb +++ b/lib/authlogic/acts_as_authentic/email.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # Sometimes models won't have an explicit "login" or "username" field. Instead they want to use the email field. - # In this case, authlogic provides validations to make sure the email submited is actually a valid email. Don't worry, - # if you do have a login or username field, Authlogic will still validate your email field. One less thing you have to - # worry about. + # Sometimes models won't have an explicit "login" or "username" field. + # Instead they want to use the email field. In this case, authlogic provides + # validations to make sure the email submited is actually a valid email. + # Don't worry, if you do have a login or username field, Authlogic will + # still validate your email field. One less thing you have to worry about. module Email def self.included(klass) klass.class_eval do extend Config - add_acts_as_authentic_module(Methods) end end - + # Configuration to modify how Authlogic handles the email field. module Config # The name of the field that stores email addresses. @@ -21,90 +23,8 @@ module Config def email_field(value = nil) rw_config(:email_field, value, first_column_to_exist(nil, :email, :email_address)) end - alias_method :email_field=, :email_field - - # Toggles validating the email field or not. - # - # * Default: true - # * Accepts: Boolean - def validate_email_field(value = nil) - rw_config(:validate_email_field, value, true) - end - alias_method :validate_email_field=, :validate_email_field - - # A hash of options for the validates_length_of call for the email field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_length_of_email_field_options to merge - # options. - # - # * Default: {:within => 6..100} - # * Accepts: Hash of options accepted by validates_length_of - def validates_length_of_email_field_options(value = nil) - rw_config(:validates_length_of_email_field_options, value, {:within => 6..100}) - end - alias_method :validates_length_of_email_field_options=, :validates_length_of_email_field_options - - # A convenience function to merge options into the validates_length_of_email_field_options. So intead of: - # - # self.validates_length_of_email_field_options = validates_length_of_email_field_options.merge(:my_option => my_value) - # - # You can do this: - # - # merge_validates_length_of_email_field_options :my_option => my_value - def merge_validates_length_of_email_field_options(options = {}) - self.validates_length_of_email_field_options = validates_length_of_email_field_options.merge(options) - end - - # A hash of options for the validates_format_of call for the email field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_format_of_email_field_options to merge - # options. - # - # * Default: {:with => Authlogic::Regex.email, :message => I18n.t('error_messages.email_invalid', :default => "should look like an email address.")} - # * Accepts: Hash of options accepted by validates_format_of - def validates_format_of_email_field_options(value = nil) - rw_config(:validates_format_of_email_field_options, value, {:with => Authlogic::Regex.email, :message => I18n.t('error_messages.email_invalid', :default => "should look like an email address.")}) - end - alias_method :validates_format_of_email_field_options=, :validates_format_of_email_field_options - - # See merge_validates_length_of_email_field_options. The same thing except for validates_format_of_email_field_options. - def merge_validates_format_of_email_field_options(options = {}) - self.validates_format_of_email_field_options = validates_format_of_email_field_options.merge(options) - end - - # A hash of options for the validates_uniqueness_of call for the email field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_uniqueness_of_email_field_options to merge - # options. - # - # * Default: {:case_sensitive => false, :scope => validations_scope, :if => "#{email_field}_changed?".to_sym} - # * Accepts: Hash of options accepted by validates_uniqueness_of - def validates_uniqueness_of_email_field_options(value = nil) - rw_config(:validates_uniqueness_of_email_field_options, value, {:case_sensitive => false, :scope => validations_scope, :if => "#{email_field}_changed?".to_sym}) - end - alias_method :validates_uniqueness_of_email_field_options=, :validates_uniqueness_of_email_field_options - - # See merge_validates_length_of_email_field_options. The same thing except for validates_uniqueness_of_email_field_options. - def merge_validates_uniqueness_of_email_field_options(options = {}) - self.validates_uniqueness_of_email_field_options = validates_uniqueness_of_email_field_options.merge(options) - end - end - - # All methods relating to the email field - module Methods - def self.included(klass) - klass.class_eval do - if validate_email_field && email_field - validates_length_of email_field, validates_length_of_email_field_options - validates_format_of email_field, validates_format_of_email_field_options - validates_uniqueness_of email_field, validates_uniqueness_of_email_field_options - end - end - end + alias email_field= email_field end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/logged_in_status.rb b/lib/authlogic/acts_as_authentic/logged_in_status.rb index 39bea2a1..f6510f7a 100644 --- a/lib/authlogic/acts_as_authentic/logged_in_status.rb +++ b/lib/authlogic/acts_as_authentic/logged_in_status.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # Since web applications are stateless there is not sure fire way to tell if a user is logged in or not, - # from the database perspective. The best way to do this is to provide a "timeout" based on inactivity. - # So if that user is inactive for a certain amount of time we assume they are logged out. That's what this - # module is all about. + # Since web applications are stateless there is not sure fire way to tell if + # a user is logged in or not, from the database perspective. The best way to + # do this is to provide a "timeout" based on inactivity. So if that user is + # inactive for a certain amount of time we assume they are logged out. + # That's what this module is all about. module LoggedInStatus def self.included(klass) klass.class_eval do @@ -11,7 +14,7 @@ def self.included(klass) add_acts_as_authentic_module(Methods) end end - + # All configuration for the logged in status feature set. module Config # The timeout to determine when a user is logged in or not. @@ -21,40 +24,62 @@ module Config def logged_in_timeout(value = nil) rw_config(:logged_in_timeout, (!value.nil? && value.to_i) || value, 10.minutes.to_i) end - alias_method :logged_in_timeout=, :logged_in_timeout + alias logged_in_timeout= logged_in_timeout end - + # All methods for the logged in status feature seat. module Methods def self.included(klass) - return if !klass.column_names.include?("last_request_at") - + return unless klass.column_names.include?("last_request_at") + klass.class_eval do include InstanceMethods - - scope :logged_in, lambda { {:conditions => ["last_request_at > ?", logged_in_timeout.seconds.ago]} } - scope :logged_out, lambda { {:conditions => ["last_request_at is NULL or last_request_at <= ?", logged_in_timeout.seconds.ago]} } + scope( + :logged_in, + lambda do + where( + "last_request_at > ? and current_login_at IS NOT NULL", + logged_in_timeout.seconds.ago + ) + end + ) + scope( + :logged_out, + lambda do + where( + "last_request_at is NULL or last_request_at <= ?", + logged_in_timeout.seconds.ago + ) + end + ) end end - + + # :nodoc: module InstanceMethods # Returns true if the last_request_at > logged_in_timeout. def logged_in? - raise "Can not determine the records login state because there is no last_request_at column" if !respond_to?(:last_request_at) + unless respond_to?(:last_request_at) + raise( + "Can not determine the records login state because " \ + "there is no last_request_at column" + ) + end !last_request_at.nil? && last_request_at > logged_in_timeout.seconds.ago end - + # Opposite of logged_in? def logged_out? !logged_in? end - + private - def logged_in_timeout - self.class.logged_in_timeout - end + + def logged_in_timeout + self.class.logged_in_timeout + end end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/login.rb b/lib/authlogic/acts_as_authentic/login.rb index ddc3f8f8..b553f108 100644 --- a/lib/authlogic/acts_as_authentic/login.rb +++ b/lib/authlogic/acts_as_authentic/login.rb @@ -1,3 +1,8 @@ +# frozen_string_literal: true + +require_relative "queries/case_sensitivity" +require_relative "queries/find_with_case" + module Authlogic module ActsAsAuthentic # Handles everything related to the login field. @@ -5,11 +10,10 @@ module Login def self.included(klass) klass.class_eval do extend Config - add_acts_as_authentic_module(Methods) end end - - # Confguration for the login field. + + # Configuration for the login field. module Config # The name of the login field in the database. # @@ -18,124 +22,42 @@ module Config def login_field(value = nil) rw_config(:login_field, value, first_column_to_exist(nil, :login, :username)) end - alias_method :login_field=, :login_field - - # Whether or not the validate the login field + alias login_field= login_field + + # This method allows you to find a record with the given login. If you + # notice, with Active Record you have the UniquenessValidator class. + # They give you a :case_sensitive option. I handle this in the same + # manner that they handle that. If you are using the login field, set + # false for the :case_sensitive option in + # validates_uniqueness_of_login_field_options and the column doesn't + # have a case-insensitive collation, this method will modify the query + # to look something like: # - # * Default: true - # * Accepts: Boolean - def validate_login_field(value = nil) - rw_config(:validate_login_field, value, true) - end - alias_method :validate_login_field=, :validate_login_field - - # A hash of options for the validates_length_of call for the login field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_length_of_login_field_options to merge - # options. - # - # * Default: {:within => 3..100} - # * Accepts: Hash of options accepted by validates_length_of - def validates_length_of_login_field_options(value = nil) - rw_config(:validates_length_of_login_field_options, value, {:within => 3..100}) - end - alias_method :validates_length_of_login_field_options=, :validates_length_of_login_field_options - - # A convenience function to merge options into the validates_length_of_login_field_options. So intead of: - # - # self.validates_length_of_login_field_options = validates_length_of_login_field_options.merge(:my_option => my_value) - # - # You can do this: - # - # merge_validates_length_of_login_field_options :my_option => my_value - def merge_validates_length_of_login_field_options(options = {}) - self.validates_length_of_login_field_options = validates_length_of_login_field_options.merge(options) - end - - # A hash of options for the validates_format_of call for the login field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_format_of_login_field_options to merge - # options. + # "LOWER(#{quoted_table_name}.#{login_field}) = LOWER(#{login})" # - # * Default: {:with => Authlogic::Regex.login, :message => I18n.t('error_messages.login_invalid', :default => "should use only letters, numbers, spaces, and .-_@ please.")} - # * Accepts: Hash of options accepted by validates_format_of - def validates_format_of_login_field_options(value = nil) - rw_config(:validates_format_of_login_field_options, value, {:with => Authlogic::Regex.login, :message => I18n.t('error_messages.login_invalid', :default => "should use only letters, numbers, spaces, and .-_@ please.")}) - end - alias_method :validates_format_of_login_field_options=, :validates_format_of_login_field_options - - # See merge_validates_length_of_login_field_options. The same thing, except for validates_format_of_login_field_options - def merge_validates_format_of_login_field_options(options = {}) - self.validates_format_of_login_field_options = validates_format_of_login_field_options.merge(options) - end - - # A hash of options for the validates_uniqueness_of call for the login field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_format_of_login_field_options to merge - # options. + # If you don't specify this it just uses a regular case-sensitive search + # (with the binary modifier if necessary): # - # * Default: {:case_sensitive => false, :scope => validations_scope, :if => "#{login_field}_changed?".to_sym} - # * Accepts: Hash of options accepted by validates_uniqueness_of - def validates_uniqueness_of_login_field_options(value = nil) - rw_config(:validates_uniqueness_of_login_field_options, value, {:case_sensitive => false, :scope => validations_scope, :if => "#{login_field}_changed?".to_sym}) - end - alias_method :validates_uniqueness_of_login_field_options=, :validates_uniqueness_of_login_field_options - - # See merge_validates_length_of_login_field_options. The same thing, except for validates_uniqueness_of_login_field_options - def merge_validates_uniqueness_of_login_field_options(options = {}) - self.validates_uniqueness_of_login_field_options = validates_uniqueness_of_login_field_options.merge(options) - end - - # This method allows you to find a record with the given login. If you notice, with ActiveRecord you have the - # validates_uniqueness_of validation function. They give you a :case_sensitive option. I handle this in the same - # manner that they handle that. If you are using the login field and set false for the :case_sensitive option in - # validates_uniqueness_of_login_field_options this method will modify the query to look something like: + # "BINARY #{login_field} = #{login}" # - # first(:conditions => ["LOWER(#{quoted_table_name}.#{login_field}) = ?", login.downcase]) - # - # If you don't specify this it calls the good old find_by_* method: - # - # find_by_login(login) - # - # The above also applies for using email as your login, except that you need to set the :case_sensitive in + # The above also applies for using email as your login, except that you + # need to set the :case_sensitive in # validates_uniqueness_of_email_field_options to false. # - # The only reason I need to do the above is for Postgres and SQLite since they perform case sensitive searches with the - # find_by_* methods. + # @api public def find_by_smart_case_login_field(login) - if login_field - find_with_case(login_field, login, validates_uniqueness_of_login_field_options[:case_sensitive] != false) - else - find_with_case(email_field, login, validates_uniqueness_of_email_field_options[:case_sensitive] != false) - end + field = login_field || email_field + sensitive = Queries::CaseSensitivity.new(self, field).sensitive? + find_with_case(field, login, sensitive) end - + private - def find_with_case(field, value, sensitivity = true) - if sensitivity - send("find_by_#{field}", value) - else - first(:conditions => ["LOWER(#{quoted_table_name}.#{field}) = ?", value.mb_chars.downcase]) - end - end - end - - # All methods relating to the login field - module Methods - # Adds in various validations, modules, etc. - def self.included(klass) - klass.class_eval do - if validate_login_field && login_field - validates_length_of login_field, validates_length_of_login_field_options - validates_format_of login_field, validates_format_of_login_field_options - validates_uniqueness_of login_field, validates_uniqueness_of_login_field_options - end - end + + # @api private + def find_with_case(field, value, sensitive) + Queries::FindWithCase.new(self, field, value, sensitive).execute end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/magic_columns.rb b/lib/authlogic/acts_as_authentic/magic_columns.rb index 4be1f8eb..df544c0a 100644 --- a/lib/authlogic/acts_as_authentic/magic_columns.rb +++ b/lib/authlogic/acts_as_authentic/magic_columns.rb @@ -1,24 +1,38 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # Magic columns are like ActiveRecord's created_at and updated_at columns. They are "magically" maintained for - # you. Authlogic has the same thing, but these are maintained on the session side. Please see Authlogic::Session::MagicColumns - # for more details. This module merely adds validations for the magic columns if they exist. + # Magic columns are like ActiveRecord's created_at and updated_at columns. + # They are "magically" maintained for you. Authlogic has the same thing, but + # these are maintained on the session side. Please see "Magic Columns" in + # `Session::Base` for more details. This module merely adds validations for + # the magic columns if they exist. module MagicColumns def self.included(klass) klass.class_eval do add_acts_as_authentic_module(Methods) end end - + # Methods relating to the magic columns module Methods def self.included(klass) klass.class_eval do - validates_numericality_of :login_count, :only_integer => :true, :greater_than_or_equal_to => 0, :allow_nil => true if column_names.include?("login_count") - validates_numericality_of :failed_login_count, :only_integer => :true, :greater_than_or_equal_to => 0, :allow_nil => true if column_names.include?("failed_login_count") + if column_names.include?("login_count") + validates_numericality_of :login_count, + only_integer: true, + greater_than_or_equal_to: 0, + allow_nil: true + end + if column_names.include?("failed_login_count") + validates_numericality_of :failed_login_count, + only_integer: true, + greater_than_or_equal_to: 0, + allow_nil: true + end end end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/password.rb b/lib/authlogic/acts_as_authentic/password.rb index b7a811a4..a96ad386 100644 --- a/lib/authlogic/acts_as_authentic/password.rb +++ b/lib/authlogic/acts_as_authentic/password.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # This module has a lot of neat functionality. It is responsible for encrypting your password, salting it, and verifying it. - # It can also help you transition to a new encryption algorithm. See the Config sub module for configuration options. + # This module has a lot of neat functionality. It is responsible for encrypting your + # password, salting it, and verifying it. It can also help you transition to a new + # encryption algorithm. See the Config sub module for configuration options. module Password def self.included(klass) klass.class_eval do @@ -10,7 +13,7 @@ def self.included(klass) add_acts_as_authentic_module(Methods) end end - + # All configuration for the password aspect of acts_as_authentic. module Config # The name of the crypted_password field in the database. @@ -18,338 +21,337 @@ module Config # * Default: :crypted_password, :encrypted_password, :password_hash, or :pw_hash # * Accepts: Symbol def crypted_password_field(value = nil) - rw_config(:crypted_password_field, value, first_column_to_exist(nil, :crypted_password, :encrypted_password, :password_hash, :pw_hash)) + rw_config( + :crypted_password_field, + value, + first_column_to_exist( + nil, + :crypted_password, + :encrypted_password, + :password_hash, + :pw_hash + ) + ) end - alias_method :crypted_password_field=, :crypted_password_field - + alias crypted_password_field= crypted_password_field + # The name of the password_salt field in the database. # # * Default: :password_salt, :pw_salt, :salt, nil if none exist # * Accepts: Symbol def password_salt_field(value = nil) - rw_config(:password_salt_field, value, first_column_to_exist(nil, :password_salt, :pw_salt, :salt)) + rw_config( + :password_salt_field, + value, + first_column_to_exist(nil, :password_salt, :pw_salt, :salt) + ) end - alias_method :password_salt_field=, :password_salt_field - - # Whether or not to require a password confirmation. If you don't want your users to confirm their password - # just set this to false. + alias password_salt_field= password_salt_field + + # Whether or not to require a password confirmation. If you don't want your users + # to confirm their password just set this to false. # # * Default: true # * Accepts: Boolean def require_password_confirmation(value = nil) rw_config(:require_password_confirmation, value, true) end - alias_method :require_password_confirmation=, :require_password_confirmation - - # By default passwords are required when a record is new or the crypted_password is blank, but if both of these things - # are met a password is not required. In this case, blank passwords are ignored. + alias require_password_confirmation= require_password_confirmation + + # By default passwords are required when a record is new or the crypted_password + # is blank, but if both of these things are met a password is not required. In + # this case, blank passwords are ignored. # - # Think about a profile page, where the user can edit all of their information, including changing their password. - # If they do not want to change their password they just leave the fields blank. This will try to set the password to - # a blank value, in which case is incorrect behavior. As such, Authlogic ignores this. But let's say you have a completely - # separate page for resetting passwords, you might not want to ignore blank passwords. If this is the case for you, then - # just set this value to false. + # Think about a profile page, where the user can edit all of their information, + # including changing their password. If they do not want to change their password + # they just leave the fields blank. This will try to set the password to a blank + # value, in which case is incorrect behavior. As such, Authlogic ignores this. But + # let's say you have a completely separate page for resetting passwords, you might + # not want to ignore blank passwords. If this is the case for you, then just set + # this value to false. # # * Default: true # * Accepts: Boolean def ignore_blank_passwords(value = nil) rw_config(:ignore_blank_passwords, value, true) end - alias_method :ignore_blank_passwords=, :ignore_blank_passwords - - # When calling valid_password?("some pass") do you want to check that password against what's in that object or whats in - # the datbase. Take this example: + alias ignore_blank_passwords= ignore_blank_passwords + + # When calling valid_password?("some pass") do you want to check that password + # against what's in that object or whats in the database. Take this example: # # u = User.first # u.password = "new pass" # u.valid_password?("old pass") # - # Should the last line above return true or false? The record hasn't been saved yet, so most would assume true. - # Other would assume false. So I let you decide by giving you this option. + # Should the last line above return true or false? The record hasn't been saved + # yet, so most would assume true. Other would assume false. So I let you decide by + # giving you this option. # # * Default: true # * Accepts: Boolean def check_passwords_against_database(value = nil) rw_config(:check_passwords_against_database, value, true) end - alias_method :check_passwords_against_database=, :check_passwords_against_database - - # Whether or not to validate the password field. + alias check_passwords_against_database= check_passwords_against_database + + # The class you want to use to encrypt and verify your encrypted + # passwords. See the Authlogic::CryptoProviders module for more info on + # the available methods and how to create your own. # - # * Default: true - # * Accepts: Boolean - def validate_password_field(value = nil) - rw_config(:validate_password_field, value, true) - end - alias_method :validate_password_field=, :validate_password_field - - # A hash of options for the validates_length_of call for the password field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_length_of_password_field_options to merge - # options. + # The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) is the + # best choice for password storage today. We recommend SCrypt. Other + # one-way functions like SHA512 are inferior, but widely used. + # Reversible functions like AES256 are the worst choice, and we no + # longer support them. # - # * Default: {:minimum => 4, :if => :require_password?} - # * Accepts: Hash of options accepted by validates_length_of - def validates_length_of_password_field_options(value = nil) - rw_config(:validates_length_of_password_field_options, value, {:minimum => 4, :if => :require_password?}) - end - alias_method :validates_length_of_password_field_options=, :validates_length_of_password_field_options - - # A convenience function to merge options into the validates_length_of_login_field_options. So intead of: - # - # self.validates_length_of_password_field_options = validates_length_of_password_field_options.merge(:my_option => my_value) + # You can use the `transition_from_crypto_providers` option to gradually + # transition to a better crypto provider without causing your users any + # pain. # - # You can do this: - # - # merge_validates_length_of_password_field_options :my_option => my_value - def merge_validates_length_of_password_field_options(options = {}) - self.validates_length_of_password_field_options = validates_length_of_password_field_options.merge(options) - end - - # A hash of options for the validates_confirmation_of call for the password field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_length_of_password_field_options to merge - # options. - # - # * Default: {:if => :require_password?} - # * Accepts: Hash of options accepted by validates_confirmation_of - def validates_confirmation_of_password_field_options(value = nil) - rw_config(:validates_confirmation_of_password_field_options, value, {:if => :require_password?}) - end - alias_method :validates_confirmation_of_password_field_options=, :validates_confirmation_of_password_field_options - - # See merge_validates_length_of_password_field_options. The same thing, except for validates_confirmation_of_password_field_options - def merge_validates_confirmation_of_password_field_options(options = {}) - self.validates_confirmation_of_password_field_options = validates_confirmation_of_password_field_options.merge(options) - end - - # A hash of options for the validates_length_of call for the password_confirmation field. Allows you to change this however you want. - # - # Keep in mind this is ruby. I wanted to keep this as flexible as possible, so you can completely replace the hash or - # merge options into it. Checkout the convenience function merge_validates_length_of_password_field_options to merge - # options. - # - # * Default: validates_length_of_password_field_options - # * Accepts: Hash of options accepted by validates_length_of - def validates_length_of_password_confirmation_field_options(value = nil) - rw_config(:validates_length_of_password_confirmation_field_options, value, validates_length_of_password_field_options) - end - alias_method :validates_length_of_password_confirmation_field_options=, :validates_length_of_password_confirmation_field_options - - # See merge_validates_length_of_password_field_options. The same thing, except for validates_length_of_password_confirmation_field_options - def merge_validates_length_of_password_confirmation_field_options(options = {}) - self.validates_length_of_password_confirmation_field_options = validates_length_of_password_confirmation_field_options.merge(options) - end - - # The class you want to use to encrypt and verify your encrypted passwords. See the Authlogic::CryptoProviders module for more info - # on the available methods and how to create your own. - # - # * Default: CryptoProviders::Sha512 + # * Default: There is no longer a default value. Prior to + # Authlogic 6, the default was `CryptoProviders::SCrypt`. If you try + # to read this config option before setting it, it will raise a + # `NilCryptoProvider` error. See that error's message for further + # details, and rationale for this change. # * Accepts: Class - def crypto_provider(value = nil) - rw_config(:crypto_provider, value, CryptoProviders::Sha512) + def crypto_provider + acts_as_authentic_config[:crypto_provider].tap { |provider| + raise NilCryptoProvider if provider.nil? + } end - alias_method :crypto_provider=, :crypto_provider - - # Let's say you originally encrypted your passwords with Sha1. Sha1 is starting to join the party with MD5 and you want to switch - # to something stronger. No problem, just specify your new and improved algorithm with the crypt_provider option and then let - # Authlogic know you are transitioning from Sha1 using this option. Authlogic will take care of everything, including transitioning - # your users to the new algorithm. The next time a user logs in, they will be granted access using the old algorithm and their - # password will be resaved with the new algorithm. All new users will obviously use the new algorithm as well. + + def crypto_provider=(value) + raise NilCryptoProvider if value.nil? + CryptoProviders::Guidance.new(value).impart_wisdom + rw_config(:crypto_provider, value) + end + + # Let's say you originally encrypted your passwords with Sha1. Sha1 is + # starting to join the party with MD5 and you want to switch to + # something stronger. No problem, just specify your new and improved + # algorithm with the crypt_provider option and then let Authlogic know + # you are transitioning from Sha1 using this option. Authlogic will take + # care of everything, including transitioning your users to the new + # algorithm. The next time a user logs in, they will be granted access + # using the old algorithm and their password will be resaved with the + # new algorithm. All new users will obviously use the new algorithm as + # well. # - # Lastly, if you want to transition again, you can pass an array of crypto providers. So you can transition from as many algorithms - # as you want. + # Lastly, if you want to transition again, you can pass an array of + # crypto providers. So you can transition from as many algorithms as you + # want. # # * Default: nil # * Accepts: Class or Array def transition_from_crypto_providers(value = nil) - rw_config(:transition_from_crypto_providers, (!value.nil? && [value].flatten.compact) || value, []) + rw_config( + :transition_from_crypto_providers, + (!value.nil? && [value].flatten.compact) || value, + [] + ) end - alias_method :transition_from_crypto_providers=, :transition_from_crypto_providers + alias transition_from_crypto_providers= transition_from_crypto_providers end - + # Callbacks / hooks to allow other modules to modify the behavior of this module. module Callbacks - METHODS = [ - "before_password_set", "after_password_set", - "before_password_verification", "after_password_verification" - ] - + # Does the order of this array matter? + METHODS = %w[ + password_set + password_verification + ].freeze + def self.included(klass) return if klass.crypted_password_field.nil? - klass.define_callbacks *METHODS - - # If Rails 3, support the new callback syntax - if klass.send(klass.respond_to?(:singleton_class) ? :singleton_class : :metaclass).method_defined?(:set_callback) - METHODS.each do |method| - klass.class_eval <<-"end_eval", __FILE__, __LINE__ - def self.#{method}(*methods, &block) - set_callback :#{method}, *methods, &block - end - end_eval - end - end - end - - private + klass.send :extend, ActiveModel::Callbacks METHODS.each do |method| - class_eval <<-"end_eval", __FILE__, __LINE__ - def #{method} - run_callbacks(:#{method}) { |result, object| result == false } - end - end_eval + klass.define_model_callbacks method, only: %i[before after] end + end end - + # The methods related to the password field. module Methods def self.included(klass) return if klass.crypted_password_field.nil? - + klass.class_eval do include InstanceMethods - - if validate_password_field - validates_length_of :password, validates_length_of_password_field_options - - if require_password_confirmation - validates_confirmation_of :password, validates_confirmation_of_password_field_options - validates_length_of :password_confirmation, validates_length_of_password_confirmation_field_options - end - end - after_save :reset_password_changed end end - + + # :nodoc: module InstanceMethods # The password def password + return nil unless defined?(@password) @password end - - # This is a virtual method. Once a password is passed to it, it will create new password salt as well as encrypt - # the password. + + # This is a virtual method. Once a password is passed to it, it will + # create new password salt as well as encrypt the password. def password=(pass) return if ignore_blank_passwords? && pass.blank? - before_password_set - @password = pass - send("#{password_salt_field}=", Authlogic::Random.friendly_token) if password_salt_field - send("#{crypted_password_field}=", crypto_provider.encrypt(*encrypt_arguments(@password, false, act_like_restful_authentication? ? :restful_authentication : nil))) - @password_changed = true - after_password_set + run_callbacks :password_set do + @password = pass + if password_salt_field + send("#{password_salt_field}=", Authlogic::Random.friendly_token) + end + send( + "#{crypted_password_field}=", + crypto_provider.encrypt(*encrypt_arguments(@password, false)) + ) + @password_changed = true + end end - - # Accepts a raw password to determine if it is the correct password or not. Notice the second argument. That defaults to the value of - # check_passwords_against_database. See that method for mor information, but basically it just tells Authlogic to check the password - # against the value in the database or the value in the object. - def valid_password?(attempted_password, check_against_database = check_passwords_against_database?) - crypted = check_against_database && send("#{crypted_password_field}_changed?") ? send("#{crypted_password_field}_was") : send(crypted_password_field) + + # Accepts a raw password to determine if it is the correct password. + # + # - attempted_password [String] - password entered by user + # - check_against_database [boolean] - Should we check the password + # against the value in the database or the value in the object? + # Default taken from config option check_passwords_against_database. + # See config method for more information. + def valid_password?( + attempted_password, + check_against_database = check_passwords_against_database? + ) + crypted = crypted_password_to_validate_against(check_against_database) return false if attempted_password.blank? || crypted.blank? - before_password_verification - - crypto_providers.each_with_index do |encryptor, index| - # The arguments_type of for the transitioning from restful_authentication - arguments_type = (act_like_restful_authentication? && index == 0) || - (transition_from_restful_authentication? && index > 0 && encryptor == Authlogic::CryptoProviders::Sha1) ? - :restful_authentication : nil - - if encryptor.matches?(crypted, *encrypt_arguments(attempted_password, check_against_database, arguments_type)) - transition_password(attempted_password) if transition_password?(index, encryptor, crypted, check_against_database) - after_password_verification - return true + run_callbacks :password_verification do + crypto_providers.each_with_index.any? do |encryptor, index| + if encryptor_matches?( + crypted, + encryptor, + attempted_password, + check_against_database + ) + if transition_password?(index, encryptor, check_against_database) + transition_password(attempted_password) + end + true + else + false + end end end - - false end - + # Resets the password to a random friendly token. def reset_password friendly_token = Authlogic::Random.friendly_token self.password = friendly_token - self.password_confirmation = friendly_token + self.password_confirmation = friendly_token if self.class.require_password_confirmation end - alias_method :randomize_password, :reset_password - + alias randomize_password reset_password + # Resets the password to a random friendly token and then saves the record. def reset_password! reset_password - save_without_session_maintenance(:validate => false) + save_without_session_maintenance(validate: false) end - alias_method :randomize_password!, :reset_password! - + alias randomize_password! reset_password! + private - def check_passwords_against_database? - self.class.check_passwords_against_database == true + + def crypted_password_to_validate_against(check_against_database) + if check_against_database && send("will_save_change_to_#{crypted_password_field}?") + send("#{crypted_password_field}_in_database") + else + send(crypted_password_field) end - - def crypto_providers - [crypto_provider] + transition_from_crypto_providers - end - - def encrypt_arguments(raw_password, check_against_database, arguments_type = nil) - salt = nil - salt = (check_against_database && send("#{password_salt_field}_changed?") ? send("#{password_salt_field}_was") : send(password_salt_field)) if password_salt_field - - case arguments_type - when :restful_authentication - [REST_AUTH_SITE_KEY, salt, raw_password, REST_AUTH_SITE_KEY].compact - else - [raw_password, salt].compact - end - end - - # Determines if we need to tranisiton the password. - # If the index > 0 then we are using an "transition from" crypto provider. - # If the encryptor has a cost and the cost it outdated. - # If we aren't using database values - # If we are using database values, only if the password hasnt change so we don't overwrite any changes - def transition_password?(index, encryptor, crypted, check_against_database) - (index > 0 || (encryptor.respond_to?(:cost_matches?) && !encryptor.cost_matches?(send(crypted_password_field)))) && - (!check_against_database || !send("#{crypted_password_field}_changed?")) - end - - def transition_password(attempted_password) - self.password = attempted_password - save(:validate => false) - end - - def require_password? - new_record? || password_changed? || send(crypted_password_field).blank? - end - - def ignore_blank_passwords? - self.class.ignore_blank_passwords == true - end - - def password_changed? - @password_changed == true - end - - def reset_password_changed - @password_changed = nil - end - - def crypted_password_field - self.class.crypted_password_field - end - - def password_salt_field - self.class.password_salt_field - end - - def crypto_provider - self.class.crypto_provider - end - - def transition_from_crypto_providers - self.class.transition_from_crypto_providers + end + + def check_passwords_against_database? + self.class.check_passwords_against_database == true + end + + def crypto_providers + [crypto_provider] + transition_from_crypto_providers + end + + # Returns an array of arguments to be passed to a crypto provider, either its + # `matches?` or its `encrypt` method. + def encrypt_arguments(raw_password, check_against_database) + salt = nil + if password_salt_field + salt = + if check_against_database && send("will_save_change_to_#{password_salt_field}?") + send("#{password_salt_field}_in_database") + else + send(password_salt_field) + end end + [raw_password, salt].compact + end + + # Given `encryptor`, does `attempted_password` match the `crypted` password? + def encryptor_matches?(crypted, encryptor, attempted_password, check_against_database) + encryptor_args = encrypt_arguments(attempted_password, check_against_database) + encryptor.matches?(crypted, *encryptor_args) + end + + # Determines if we need to transition the password. + # + # - If the index > 0 then we are using a "transition from" crypto + # provider. + # - If the encryptor has a cost and the cost it outdated. + # - If we aren't using database values + # - If we are using database values, only if the password hasn't + # changed so we don't overwrite any changes + def transition_password?(index, encryptor, check_against_database) + ( + index > 0 || + (encryptor.respond_to?(:cost_matches?) && + !encryptor.cost_matches?(send(crypted_password_field))) + ) && + ( + !check_against_database || + !send("will_save_change_to_#{crypted_password_field}?") + ) + end + + def transition_password(attempted_password) + self.password = attempted_password + save(validate: false) + end + + def require_password? + # this is _not_ the activemodel changed? method, see below + new_record? || password_changed? || send(crypted_password_field).blank? + end + + def ignore_blank_passwords? + self.class.ignore_blank_passwords == true + end + + def password_changed? + defined?(@password_changed) && @password_changed == true + end + + def reset_password_changed + @password_changed = nil + end + + def crypted_password_field + self.class.crypted_password_field + end + + def password_salt_field + self.class.password_salt_field + end + + def crypto_provider + self.class.crypto_provider + end + + def transition_from_crypto_providers + self.class.transition_from_crypto_providers + end end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/perishable_token.rb b/lib/authlogic/acts_as_authentic/perishable_token.rb index 864c6e71..78aaf803 100644 --- a/lib/authlogic/acts_as_authentic/perishable_token.rb +++ b/lib/authlogic/acts_as_authentic/perishable_token.rb @@ -1,9 +1,17 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # This provides a handy token that is "perishable". Meaning the token is only good for a certain amount of time. This is perfect for - # resetting password, confirming accounts, etc. Typically during these actions you send them this token in via their email. Once they - # use the token and do what they need to do, that token should expire. Don't worry about maintaining this, changing it, or expiring it - # yourself. Authlogic does all of this for you. See the sub modules for all of the tools Authlogic provides to you. + # This provides a handy token that is "perishable", meaning the token is + # only good for a certain amount of time. + # + # This is useful for resetting password, confirming accounts, etc. Typically + # during these actions you send them this token in an email. Once they use + # the token and do what they need to do, that token should expire. + # + # Don't worry about maintaining the token, changing it, or expiring it + # yourself. Authlogic does all of this for you. See the sub modules for all + # of the tools Authlogic provides to you. module PerishableToken def self.included(klass) klass.class_eval do @@ -11,90 +19,99 @@ def self.included(klass) add_acts_as_authentic_module(Methods) end end - - # Change how the perishable token works. + + # Configure the perishable token. module Config - # When using the find_using_perishable_token method the token can expire. If the token is expired, no - # record will be returned. Use this option to specify how long the token is valid for. + # When using the find_using_perishable_token method the token can + # expire. If the token is expired, no record will be returned. Use this + # option to specify how long the token is valid for. # # * Default: 10.minutes # * Accepts: Fixnum def perishable_token_valid_for(value = nil) - rw_config(:perishable_token_valid_for, (!value.nil? && value.to_i) || value, 10.minutes.to_i) + rw_config( + :perishable_token_valid_for, + (!value.nil? && value.to_i) || value, + 10.minutes.to_i + ) end - alias_method :perishable_token_valid_for=, :perishable_token_valid_for - - # Authlogic tries to expire and change the perishable token as much as possible, without comprising - # it's purpose. This is for security reasons. If you want to manage it yourself, you can stop - # Authlogic from getting your in way by setting this to true. + alias perishable_token_valid_for= perishable_token_valid_for + + # Authlogic tries to expire and change the perishable token as much as + # possible, without compromising its purpose. If you want to manage it + # yourself, set this to true. # # * Default: false # * Accepts: Boolean def disable_perishable_token_maintenance(value = nil) rw_config(:disable_perishable_token_maintenance, value, false) end - alias_method :disable_perishable_token_maintenance=, :disable_perishable_token_maintenance + alias disable_perishable_token_maintenance= disable_perishable_token_maintenance end - + # All methods relating to the perishable token. module Methods def self.included(klass) - return if !klass.column_names.include?("perishable_token") - + return unless klass.column_names.include?("perishable_token") + klass.class_eval do extend ClassMethods include InstanceMethods - - validates_uniqueness_of :perishable_token, :if => :perishable_token_changed? - before_save :reset_perishable_token, :unless => :disable_perishable_token_maintenance? + + validates_uniqueness_of :perishable_token, case_sensitive: true, + if: :will_save_change_to_perishable_token? + before_save :reset_perishable_token, unless: :disable_perishable_token_maintenance? end end - - # Class level methods for the perishable token + + # :nodoc: module ClassMethods - # Use this methdo to find a record with a perishable token. This method does 2 things for you: + # Use this method to find a record with a perishable token. This + # method does 2 things for you: # # 1. It ignores blank tokens # 2. It enforces the perishable_token_valid_for configuration option. # - # If you want to use a different timeout value, just pass it as the second parameter: + # If you want to use a different timeout value, just pass it as the + # second parameter: # # User.find_using_perishable_token(token, 1.hour) - def find_using_perishable_token(token, age = self.perishable_token_valid_for) + def find_using_perishable_token(token, age = perishable_token_valid_for) return if token.blank? age = age.to_i - + conditions_sql = "perishable_token = ?" - conditions_subs = [token] - + conditions_subs = [token.to_s] + if column_names.include?("updated_at") && age > 0 conditions_sql += " and updated_at > ?" conditions_subs << age.seconds.ago end - - find(:first, :conditions => [conditions_sql, *conditions_subs]) + + where(conditions_sql, *conditions_subs).first end - + # This method will raise ActiveRecord::NotFound if no record is found. def find_using_perishable_token!(token, age = perishable_token_valid_for) find_using_perishable_token(token, age) || raise(ActiveRecord::RecordNotFound) end end - - # Instance level methods for the perishable token. + + # :nodoc: module InstanceMethods # Resets the perishable token to a random friendly token. def reset_perishable_token self.perishable_token = Random.friendly_token end - + # Same as reset_perishable_token, but then saves the record afterwards. def reset_perishable_token! reset_perishable_token - save_without_session_maintenance(:validate => false) + save_without_session_maintenance(validate: false) end - - # A convenience method based on the disable_perishable_token_maintenance configuration option. + + # A convenience method based on the + # disable_perishable_token_maintenance configuration option. def disable_perishable_token_maintenance? self.class.disable_perishable_token_maintenance == true end diff --git a/lib/authlogic/acts_as_authentic/persistence_token.rb b/lib/authlogic/acts_as_authentic/persistence_token.rb index 1ff11d39..9beef61f 100644 --- a/lib/authlogic/acts_as_authentic/persistence_token.rb +++ b/lib/authlogic/acts_as_authentic/persistence_token.rb @@ -1,68 +1,70 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic # Maintains the persistence token, the token responsible for persisting sessions. This token - # gets stores in the session and the cookie. + # gets stored in the session and the cookie. module PersistenceToken def self.included(klass) klass.class_eval do add_acts_as_authentic_module(Methods) end end - + # Methods for the persistence token. module Methods def self.included(klass) klass.class_eval do extend ClassMethods include InstanceMethods - + + # If the table does not have a password column, then + # `after_password_set` etc. will not be defined. See + # `Authlogic::ActsAsAuthentic::Password::Callbacks.included` if respond_to?(:after_password_set) && respond_to?(:after_password_verification) after_password_set :reset_persistence_token - after_password_verification :reset_persistence_token!, :if => :reset_persistence_token? + after_password_verification :reset_persistence_token!, if: :reset_persistence_token? end - + validates_presence_of :persistence_token - validates_uniqueness_of :persistence_token, :if => :persistence_token_changed? - - before_validation :reset_persistence_token, :if => :reset_persistence_token? + validates_uniqueness_of :persistence_token, case_sensitive: true, + if: :will_save_change_to_persistence_token? + + before_validation :reset_persistence_token, if: :reset_persistence_token? end end - - # Class level methods for the persistence token. + + # :nodoc: module ClassMethods - # Resets ALL persistence tokens in the database, which will require all users to reauthenticate. + # Resets ALL persistence tokens in the database, which will require + # all users to re-authenticate. def forget_all # Paginate these to save on memory - records = nil - i = 0 - begin - records = find(:all, :limit => 50, :offset => i) - records.each { |record| record.forget! } - i += 50 - end while !records.blank? + find_each(batch_size: 50, &:forget!) end end - - # Instance level methods for the persistence token. + + # :nodoc: module InstanceMethods # Resets the persistence_token field to a random hex value. def reset_persistence_token self.persistence_token = Authlogic::Random.hex_token end - + # Same as reset_persistence_token, but then saves the record. def reset_persistence_token! reset_persistence_token - save_without_session_maintenance(:validate => false) + save_without_session_maintenance(validate: false) end - alias_method :forget!, :reset_persistence_token! - + alias forget! reset_persistence_token! + private - def reset_persistence_token? - persistence_token.blank? - end + + def reset_persistence_token? + persistence_token.blank? + end end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/queries/case_sensitivity.rb b/lib/authlogic/acts_as_authentic/queries/case_sensitivity.rb new file mode 100644 index 00000000..dfb092ca --- /dev/null +++ b/lib/authlogic/acts_as_authentic/queries/case_sensitivity.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Authlogic + module ActsAsAuthentic + module Queries + # @api private + class CaseSensitivity + E_UNABLE_TO_DETERMINE_SENSITIVITY = <<~EOS + Authlogic was unable to determine what case-sensitivity to use when + searching for email/login. To specify a sensitivity, validate the + uniqueness of the email/login and use the `case_sensitive` option, + like this: + + validates :email, uniqueness: { case_sensitive: false } + + Authlogic will now perform a case-insensitive query. + EOS + + # @api private + def initialize(model_class, attribute) + @model_class = model_class + @attribute = attribute.to_sym + end + + # @api private + def sensitive? + sensitive = uniqueness_validator_options[:case_sensitive] + if sensitive.nil? + ::Kernel.warn(E_UNABLE_TO_DETERMINE_SENSITIVITY) + false + else + sensitive + end + end + + private + + # @api private + def uniqueness_validator + @model_class.validators.select { |v| + v.is_a?(::ActiveRecord::Validations::UniquenessValidator) && + v.attributes == [@attribute] + }.first + end + + # @api private + def uniqueness_validator_options + uniqueness_validator&.options || {} + end + end + end + end +end diff --git a/lib/authlogic/acts_as_authentic/queries/find_with_case.rb b/lib/authlogic/acts_as_authentic/queries/find_with_case.rb new file mode 100644 index 00000000..586c00fb --- /dev/null +++ b/lib/authlogic/acts_as_authentic/queries/find_with_case.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +module Authlogic + module ActsAsAuthentic + module Queries + # The query used by public-API method `find_by_smart_case_login_field`. + # + # We use the rails methods `case_insensitive_comparison` and + # `case_sensitive_comparison`. These methods nicely take into account + # MySQL collations. (Consider the case where a user *says* they want a + # case-sensitive uniqueness validation, but then they configure their + # database to have an insensitive collation. Rails will handle this for + # us, by downcasing, see + # `active_record/connection_adapters/abstract_mysql_adapter.rb`) So that's + # great! But, these methods are not part of rails' public API, so there + # are no docs. So, everything we know about how to use the methods + # correctly comes from mimicing what we find in + # `active_record/validations/uniqueness.rb`. + # + # @api private + class FindWithCase + # Dup ActiveRecord.gem_version before freezing, in case someone + # else wants to modify it. Freezing modifies an object in place. + # https://github.com/binarylogic/authlogic/pull/590 + AR_GEM_VERSION = ::ActiveRecord.gem_version.dup.freeze + + # @api private + def initialize(model_class, field, value, sensitive) + @model_class = model_class + @field = field.to_s + @value = value + @sensitive = sensitive + end + + # @api private + def execute + @model_class.where(comparison).first + end + + private + + # @api private + # @return Arel::Nodes::Equality + def comparison + @sensitive ? sensitive_comparison : insensitive_comparison + end + + # @api private + def insensitive_comparison + if AR_GEM_VERSION > Gem::Version.new("5.3") + @model_class.connection.case_insensitive_comparison( + @model_class.arel_table[@field], @value + ) + else + @model_class.connection.case_insensitive_comparison( + @model_class.arel_table, + @field, + @model_class.columns_hash[@field], + @value + ) + end + end + + # @api private + def sensitive_comparison + bound_value = @model_class.predicate_builder.build_bind_attribute(@field, @value) + if AR_GEM_VERSION > Gem::Version.new("5.3") + @model_class.connection.case_sensitive_comparison( + @model_class.arel_table[@field], bound_value + ) + else + @model_class.connection.case_sensitive_comparison( + @model_class.arel_table, + @field, + @model_class.columns_hash[@field], + bound_value + ) + end + end + end + end + end +end diff --git a/lib/authlogic/acts_as_authentic/restful_authentication.rb b/lib/authlogic/acts_as_authentic/restful_authentication.rb deleted file mode 100644 index 1e6d5a0a..00000000 --- a/lib/authlogic/acts_as_authentic/restful_authentication.rb +++ /dev/null @@ -1,61 +0,0 @@ -module Authlogic - module ActsAsAuthentic - # This module is responsible for transitioning existing applications from the restful_authentication plugin. - module RestfulAuthentication - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - end - end - - module Config - # Switching an existing app to Authlogic from restful_authentication? No problem, just set this true and your users won't know - # anything changed. From your database perspective nothing will change at all. Authlogic will continue to encrypt passwords - # just like restful_authentication, so your app won't skip a beat. Although, might consider transitioning your users to a newer - # and stronger algorithm. Checkout the transition_from_restful_authentication option. - # - # * Default: false - # * Accepts: Boolean - def act_like_restful_authentication(value = nil) - r = rw_config(:act_like_restful_authentication, value, false) - set_restful_authentication_config if value - r - end - alias_method :act_like_restful_authentication=, :act_like_restful_authentication - - # This works just like act_like_restful_authentication except that it will start transitioning your users to the algorithm you - # specify with the crypto provider option. The next time they log in it will resave their password with the new algorithm - # and any new record will use the new algorithm as well. Make sure to update your users table if you are using the default - # migration since it will set crypted_password and salt columns to a maximum width of 40 characters which is not enough. - def transition_from_restful_authentication(value = nil) - r = rw_config(:transition_from_restful_authentication, value, false) - set_restful_authentication_config if value - r - end - alias_method :transition_from_restful_authentication=, :transition_from_restful_authentication - - private - def set_restful_authentication_config - crypto_provider_key = act_like_restful_authentication ? :crypto_provider : :transition_from_crypto_providers - self.send("#{crypto_provider_key}=", CryptoProviders::Sha1) - if !defined?(::REST_AUTH_SITE_KEY) || ::REST_AUTH_SITE_KEY.nil? - class_eval("::REST_AUTH_SITE_KEY = ''") if !defined?(::REST_AUTH_SITE_KEY) - CryptoProviders::Sha1.stretches = 1 - end - end - end - - module InstanceMethods - private - def act_like_restful_authentication? - self.class.act_like_restful_authentication == true - end - - def transition_from_restful_authentication? - self.class.transition_from_restful_authentication == true - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/acts_as_authentic/session_maintenance.rb b/lib/authlogic/acts_as_authentic/session_maintenance.rb index 0bffc92c..d15f3b32 100644 --- a/lib/authlogic/acts_as_authentic/session_maintenance.rb +++ b/lib/authlogic/acts_as_authentic/session_maintenance.rb @@ -1,22 +1,28 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # This is one of my favorite features that I think is pretty cool. It's things like this that make a library great - # and let you know you are on the right track. + # This is one of my favorite features that I think is pretty cool. It's + # things like this that make a library great and let you know you are on the + # right track. # - # Just to clear up any confusion, Authlogic stores both the record id and the persistence token in the session. - # Why? So stale sessions can not be persisted. It stores the id so it can quickly find the record, and the - # persistence token to ensure no sessions are stale. So if the persistence token changes, the user must log - # back in. + # Just to clear up any confusion, Authlogic stores both the record id and + # the persistence token in the session. Why? So stale sessions can not be + # persisted. It stores the id so it can quickly find the record, and the + # persistence token to ensure no sessions are stale. So if the persistence + # token changes, the user must log back in. # - # Well, the persistence token changes with the password. What happens if the user changes his own password? - # He shouldn't have to log back in, he's the one that made the change. + # Well, the persistence token changes with the password. What happens if the + # user changes his own password? He shouldn't have to log back in, he's the + # one that made the change. # - # That being said, wouldn't it be nice if their session and cookie information was automatically updated? - # Instead of cluttering up your controller with redundant session code. The same thing goes for new + # That being said, wouldn't it be nice if their session and cookie + # information was automatically updated? Instead of cluttering up your + # controller with redundant session code. The same thing goes for new # registrations. # - # That's what this module is all about. This will automatically maintain the cookie and session values as - # records are saved. + # That's what this module is all about. This will automatically maintain the + # cookie and session values as records are saved. module SessionMaintenance def self.included(klass) klass.class_eval do @@ -24,21 +30,33 @@ def self.included(klass) add_acts_as_authentic_module(Methods) end end - + + # Configuration for the session maintenance aspect of acts_as_authentic. + # These methods become class methods of ::ActiveRecord::Base. module Config - # This is more of a convenience method. In order to turn off automatic maintenance of sessions just - # set this to false, or you can also set the session_ids method to a blank array. Both accomplish - # the same thing. This method is a little clearer in it's intentions though. + # In order to turn off automatic maintenance of sessions + # after create, just set this to false. + # + # * Default: true + # * Accepts: Boolean + def log_in_after_create(value = nil) + rw_config(:log_in_after_create, value, true) + end + alias log_in_after_create= log_in_after_create + + # In order to turn off automatic maintenance of sessions when updating + # the password, just set this to false. # # * Default: true # * Accepts: Boolean - def maintain_sessions(value = nil) - rw_config(:maintain_sessions, value, true) + def log_in_after_password_change(value = nil) + rw_config(:log_in_after_password_change, value, true) end - alias_method :maintain_sessions=, :maintain_sessions - - # As you may know, authlogic sessions can be separate by id (See Authlogic::Session::Base#id). You can - # specify here what session ids you want auto maintained. By default it is the main session, which has + alias log_in_after_password_change= log_in_after_password_change + + # As you may know, authlogic sessions can be separate by id (See + # Authlogic::Session::Base#id). You can specify here what session ids + # you want auto maintained. By default it is the main session, which has # an id of nil. # # * Default: [nil] @@ -46,94 +64,123 @@ def maintain_sessions(value = nil) def session_ids(value = nil) rw_config(:session_ids, value, [nil]) end - alias_method :session_ids=, :session_ids - - # The name of the associated session class. This is inferred by the name of the model. + alias session_ids= session_ids + + # The name of the associated session class. This is inferred by the name + # of the model. # # * Default: "#{klass.name}Session".constantize # * Accepts: Class def session_class(value = nil) - const = "#{base_class.name}Session".constantize rescue nil + const = begin + "#{base_class.name}Session".constantize + rescue NameError + nil + end rw_config(:session_class, value, const) end - alias_method :session_class=, :session_class + alias session_class= session_class end - + + # This module, as one of the `acts_as_authentic_modules`, is only included + # into an ActiveRecord model if that model calls `acts_as_authentic`. module Methods def self.included(klass) klass.class_eval do - before_save :get_session_information, :if => :update_sessions? - before_save :maintain_sessions, :if => :update_sessions? + before_save :get_session_information, if: :update_sessions? + before_save :maintain_sessions, if: :update_sessions? end end - + # Save the record and skip session maintenance all together. - def save_without_session_maintenance(*args) + def save_without_session_maintenance(**options) self.skip_session_maintenance = true - result = save(*args) + result = save(**options) self.skip_session_maintenance = false result end - + private - def skip_session_maintenance=(value) - @skip_session_maintenance = value - end - - def skip_session_maintenance - @skip_session_maintenance ||= false - end - - def update_sessions? - !skip_session_maintenance && session_class && session_class.activated? && self.class.maintain_sessions == true && !session_ids.blank? && persistence_token_changed? - end - - def get_session_information - # Need to determine if we are completely logged out, or logged in as another user - @_sessions = [] - - session_ids.each do |session_id| - session = session_class.find(session_id, self) - @_sessions << session if session && session.record - end - end - - def maintain_sessions - if @_sessions.empty? - create_session - else - update_sessions - end - end - - def create_session - # We only want to automatically login into the first session, since this is the main session. The other sessions are sessions - # that need to be created after logging into the main session. - session_id = session_ids.first - session_class.create(*[self, self, session_id].compact) - - return true - end - - def update_sessions - # We found sessions above, let's update them with the new info - @_sessions.each do |stale_session| - next if stale_session.record != self - stale_session.unauthorized_record = self - stale_session.save - end - - return true + + def skip_session_maintenance=(value) + @skip_session_maintenance = value + end + + def skip_session_maintenance + @skip_session_maintenance ||= false + end + + def update_sessions? + !skip_session_maintenance && + session_class && + session_class.activated? && + maintain_session? && + !session_ids.blank? && + will_save_change_to_persistence_token? + end + + def maintain_session? + log_in_after_create? || log_in_after_password_change? + end + + def get_session_information + # Need to determine if we are completely logged out, or logged in as + # another user. + @_sessions = [] + + session_ids.each do |session_id| + session = session_class.find(session_id, self) + @_sessions << session if session&.record end - - def session_ids - self.class.session_ids + end + + def maintain_sessions + if @_sessions.empty? + create_session + else + update_sessions end - - def session_class - self.class.session_class + end + + def create_session + # We only want to automatically login into the first session, since + # this is the main session. The other sessions are sessions that + # need to be created after logging into the main session. + session_id = session_ids.first + session_class.create(*[self, self, session_id].compact) + + true + end + + def update_sessions + # We found sessions above, let's update them with the new info + @_sessions.each do |stale_session| + next if stale_session.record != self + stale_session.unauthorized_record = self + stale_session.save end + + true + end + + def session_ids + self.class.session_ids + end + + def session_class + self.class.session_class + end + + def log_in_after_create? + new_record? && self.class.log_in_after_create + end + + def log_in_after_password_change? + persisted? && + will_save_change_to_persistence_token? && + self.class.log_in_after_password_change + end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/single_access_token.rb b/lib/authlogic/acts_as_authentic/single_access_token.rb index e5c72481..b93df7b1 100644 --- a/lib/authlogic/acts_as_authentic/single_access_token.rb +++ b/lib/authlogic/acts_as_authentic/single_access_token.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + module Authlogic module ActsAsAuthentic - # This module is responsible for maintaining the single_access token. For more information the single access token and how to use it, - # see the Authlogic::Session::Params module. + # This module is responsible for maintaining the single_access token. For + # more information the single access token and how to use it, see "Params" + # in `Session::Base`. module SingleAccessToken def self.included(klass) klass.class_eval do @@ -9,57 +12,72 @@ def self.included(klass) add_acts_as_authentic_module(Methods) end end - + # All configuration for the single_access token aspect of acts_as_authentic. + # + # These methods become class methods of ::ActiveRecord::Base. module Config - # The single access token is used for authentication via URLs, such as a private feed. That being said, - # if the user changes their password, that token probably shouldn't change. If it did, the user would have - # to update all of their URLs. So be default this is option is disabled, if you need it, feel free to turn - # it on. + # The single access token is used for authentication via URLs, such as a private + # feed. That being said, if the user changes their password, that token probably + # shouldn't change. If it did, the user would have to update all of their URLs. So + # be default this is option is disabled, if you need it, feel free to turn it on. # # * Default: false # * Accepts: Boolean def change_single_access_token_with_password(value = nil) rw_config(:change_single_access_token_with_password, value, false) end - alias_method :change_single_access_token_with_password=, :change_single_access_token_with_password + alias change_single_access_token_with_password= change_single_access_token_with_password end - + # All method, for the single_access token aspect of acts_as_authentic. + # + # This module, as one of the `acts_as_authentic_modules`, is only included + # into an ActiveRecord model if that model calls `acts_as_authentic`. module Methods def self.included(klass) - return if !klass.column_names.include?("single_access_token") - + return unless klass.column_names.include?("single_access_token") + klass.class_eval do include InstanceMethods - validates_uniqueness_of :single_access_token, :if => :single_access_token_changed? - before_validation :reset_single_access_token, :if => :reset_single_access_token? - after_password_set(:reset_single_access_token, :if => :change_single_access_token_with_password?) if respond_to?(:after_password_set) + validates_uniqueness_of :single_access_token, + case_sensitive: true, + if: :will_save_change_to_single_access_token? + + before_validation :reset_single_access_token, if: :reset_single_access_token? + if respond_to?(:after_password_set) + after_password_set( + :reset_single_access_token, + if: :change_single_access_token_with_password? + ) + end end end - + + # :nodoc: module InstanceMethods # Resets the single_access_token to a random friendly token. def reset_single_access_token self.single_access_token = Authlogic::Random.friendly_token end - + # same as reset_single_access_token, but then saves the record. def reset_single_access_token! reset_single_access_token save_without_session_maintenance end - + protected - def reset_single_access_token? - single_access_token.blank? - end - - def change_single_access_token_with_password? - self.class.change_single_access_token_with_password == true - end + + def reset_single_access_token? + single_access_token.blank? + end + + def change_single_access_token_with_password? + self.class.change_single_access_token_with_password == true + end end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/acts_as_authentic/validations_scope.rb b/lib/authlogic/acts_as_authentic/validations_scope.rb deleted file mode 100644 index 8d8d7003..00000000 --- a/lib/authlogic/acts_as_authentic/validations_scope.rb +++ /dev/null @@ -1,32 +0,0 @@ -module Authlogic - module ActsAsAuthentic - # Allows you to scope everything to specific fields. - # See the Config submodule for more info. - # For information on how to scope off of a parent object see Authlogic::AuthenticatesMany - module ValidationsScope - def self.included(klass) - klass.class_eval do - extend Config - end - end - - # All configuration for the scope feature. - module Config - # Allows you to scope everything to specific field(s). Works just like validates_uniqueness_of. - # For example, let's say a user belongs to a company, and you want to scope everything to the - # company: - # - # acts_as_authentic do |c| - # c.validations_scope = :company_id - # end - # - # * Default: nil - # * Accepts: Symbol or Array of symbols - def validations_scope(value = nil) - rw_config(:validations_scope, value) - end - alias_method :validations_scope=, :validations_scope - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/authenticates_many/association.rb b/lib/authlogic/authenticates_many/association.rb deleted file mode 100644 index 9bde2438..00000000 --- a/lib/authlogic/authenticates_many/association.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Authlogic - module AuthenticatesMany - # An object of this class is used as a proxy for the authenticates_many relationship. It basically allows you to "save" scope details - # and call them on an object, which allows you to do the following: - # - # @account.user_sessions.new - # @account.user_sessions.find - # # ... etc - # - # You can call all of the class level methods off of an object with a saved scope, so that calling the above methods scopes the user - # sessions down to that specific account. To implement this via ActiveRecord do something like: - # - # class User < ActiveRecord::Base - # authenticates_many :user_sessions - # end - class Association - attr_accessor :klass, :find_options, :id - - def initialize(klass, find_options, id) - self.klass = klass - self.find_options = find_options - self.id = id - end - - [:create, :create!, :find, :new].each do |method| - class_eval <<-"end_eval", __FILE__, __LINE__ - def #{method}(*args) - klass.with_scope(scope_options) do - klass.#{method}(*args) - end - end - end_eval - end - alias_method :build, :new - - private - def scope_options - {:find_options => find_options, :id => id} - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/authenticates_many/base.rb b/lib/authlogic/authenticates_many/base.rb deleted file mode 100644 index b03482dc..00000000 --- a/lib/authlogic/authenticates_many/base.rb +++ /dev/null @@ -1,55 +0,0 @@ -module Authlogic - # This allows you to scope your authentication. For example, let's say all users belong to an account, you want to make sure only users - # that belong to that account can actually login into that account. Simple, just do: - # - # class Account < ActiveRecord::Base - # authenticates_many :user_sessions - # end - # - # Now you can scope sessions just like everything else in ActiveRecord: - # - # @account.user_sessions.new(*args) - # @account.user_sessions.create(*args) - # @account.user_sessions.find(*args) - # # ... etc - # - # Checkout the authenticates_many method for a list of options. - # You may also want to checkout Authlogic::ActsAsAuthentic::Scope to scope your model. - module AuthenticatesMany - module Base - # Allows you set essentially set up a relationship with your sessions. See module definition above for more details. - # - # === Options - # - # * session_class: default: "#{name}Session", - # This is the related session class. - # - # * relationship_name: default: options[:session_class].klass_name.underscore.pluralize, - # This is the name of the relationship you want to use to scope everything. For example an Account has many Users. There should be a relationship - # called :users that you defined with a has_many. The reason we use the relationship is so you don't have to repeat yourself. The relatonship - # could have all kinds of custom options. So instead of repeating yourself we essentially use the scope that the relationship creates. - # - # * find_options: default: nil, - # By default the find options are created from the relationship you specify with :relationship_name. But if you want to override this and - # manually specify find_options you can do it here. Specify options just as you would in ActiveRecord::Base.find. - # - # * scope_cookies: default: false - # By the nature of cookies they scope theirself if you are using subdomains to access accounts. If you aren't using subdomains you need to have - # separate cookies for each account, assuming a user is logging into mroe than one account. Authlogic can take care of this for you by - # prefixing the name of the cookie and sessin with the model id. You just need to tell Authlogic to do this by passing this option. - def authenticates_many(name, options = {}) - options[:session_class] ||= name.to_s.classify.constantize - options[:relationship_name] ||= options[:session_class].klass_name.underscore.pluralize - class_eval <<-"end_eval", __FILE__, __LINE__ - def #{name} - find_options = #{options[:find_options].inspect} || #{options[:relationship_name]}.scope(:find) - find_options.delete_if { |key, value| ![:conditions, :include, :joins].include?(key.to_sym) || value.nil? } - @#{name} ||= Authlogic::AuthenticatesMany::Association.new(#{options[:session_class]}, find_options, #{options[:scope_cookies] ? "self.class.model_name.underscore + '_' + self.send(self.class.primary_key).to_s" : "nil"}) - end - end_eval - end - end - - ::ActiveRecord::Base.extend(Base) if defined?(::ActiveRecord) - end -end \ No newline at end of file diff --git a/lib/authlogic/config.rb b/lib/authlogic/config.rb new file mode 100644 index 00000000..2e2aa2a5 --- /dev/null +++ b/lib/authlogic/config.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Authlogic + # Mixed into `Authlogic::ActsAsAuthentic::Base` and + # `Authlogic::Session::Base`. + module Config + E_USE_NORMAL_RAILS_VALIDATION = <<~EOS + This Authlogic configuration option (%s) is deprecated. Use normal + ActiveRecord validation instead. Detailed instructions: + https://github.com/binarylogic/authlogic/blob/master/doc/use_normal_rails_validation.md + EOS + + def self.extended(klass) + klass.class_eval do + # TODO: Is this a confusing name, given this module is mixed into + # both `Authlogic::ActsAsAuthentic::Base` and + # `Authlogic::Session::Base`? Perhaps a more generic name, like + # `authlogic_config` would be better? + class_attribute :acts_as_authentic_config + self.acts_as_authentic_config ||= {} + end + end + + private + + def deprecate_authlogic_config(method_name) + ::ActiveSupport::Deprecation.new.warn( + format(E_USE_NORMAL_RAILS_VALIDATION, method_name) + ) + end + + # This is a one-liner method to write a config setting, read the config + # setting, and also set a default value for the setting. + def rw_config(key, value, default_value = nil) + if value.nil? + acts_as_authentic_config.include?(key) ? acts_as_authentic_config[key] : default_value + else + self.acts_as_authentic_config = acts_as_authentic_config.merge(key => value) + value + end + end + end +end diff --git a/lib/authlogic/controller_adapters/abstract_adapter.rb b/lib/authlogic/controller_adapters/abstract_adapter.rb index 4b8e3a63..fff08e8b 100644 --- a/lib/authlogic/controller_adapters/abstract_adapter.rb +++ b/lib/authlogic/controller_adapters/abstract_adapter.rb @@ -1,29 +1,36 @@ +# frozen_string_literal: true + module Authlogic module ControllerAdapters # :nodoc: - # Allows you to use Authlogic in any framework you want, not just rails. See the RailsAdapter or MerbAdapter - # for an example of how to adapt Authlogic to work with your framework. + # Allows you to use Authlogic in any framework you want, not just rails. See + # the RailsAdapter for an example of how to adapt Authlogic to work with + # your framework. class AbstractAdapter + E_COOKIE_DOMAIN_ADAPTER = "The cookie_domain method has not been " \ + "implemented by the controller adapter" + ENV_SESSION_OPTIONS = "rack.session.options" + attr_accessor :controller def initialize(controller) self.controller = controller end - - def authenticate_with_http_basic(&block) + + def authenticate_with_http_basic @auth = Rack::Auth::Basic::Request.new(controller.request.env) - if @auth.provided? and @auth.basic? - block.call(*@auth.credentials) + if @auth.provided? && @auth.basic? + yield(*@auth.credentials) else false end end - + def cookies controller.cookies end - + def cookie_domain - raise NotImplementedError.new("The cookie_domain method has not been implemented by the controller adapter") + raise NotImplementedError, E_COOKIE_DOMAIN_ADAPTER end def params @@ -38,30 +45,75 @@ def request_content_type request.content_type end + # Inform Rack that we would like a new session ID to be assigned. Changes + # the ID, but not the contents of the session. + # + # The `:renew` option is read by `rack/session/abstract/id.rb`. + # + # This is how Devise (via warden) implements defense against Session + # Fixation. Our implementation is copied directly from the warden gem + # (set_user in warden/proxy.rb) + def renew_session_id + env = request.env + options = env[ENV_SESSION_OPTIONS] + if options + if options.frozen? + env[ENV_SESSION_OPTIONS] = options.merge(renew: true).freeze + else + options[:renew] = true + end + end + end + def session controller.session end - + def responds_to_single_access_allowed? controller.respond_to?(:single_access_allowed?, true) end - + def single_access_allowed? controller.send(:single_access_allowed?) end - - def responds_to_last_request_update_allowed? - controller.respond_to?(:last_request_update_allowed?, true) - end - + + # You can disable the updating of `last_request_at` + # on a per-controller basis. + # + # # in your controller + # def last_request_update_allowed? + # false + # end + # + # For example, what if you had a javascript function that polled the + # server updating how much time is left in their session before it + # times out. Obviously you would want to ignore this request, because + # then the user would never time out. So you can do something like + # this in your controller: + # + # def last_request_update_allowed? + # action_name != "update_session_time_left" + # end + # + # See `authlogic/session/magic_columns.rb` to learn more about the + # `last_request_at` column itself. def last_request_update_allowed? - controller.send(:last_request_update_allowed?) + if controller.respond_to?(:last_request_update_allowed?, true) + controller.send(:last_request_update_allowed?) + else + true + end + end + + def respond_to_missing?(*args) + super(*args) || controller.respond_to?(*args) end - + private - def method_missing(id, *args, &block) - controller.send(id, *args, &block) - end + + def method_missing(id, *args, &block) + controller.send(id, *args, &block) + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/controller_adapters/merb_adapter.rb b/lib/authlogic/controller_adapters/merb_adapter.rb deleted file mode 100644 index e4404491..00000000 --- a/lib/authlogic/controller_adapters/merb_adapter.rb +++ /dev/null @@ -1,30 +0,0 @@ -module Authlogic - module ControllerAdapters - # Adapts authlogic to work with merb. The point is to close the gap between what authlogic expects and what the merb controller object - # provides. Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, etc. - class MerbAdapter < AbstractAdapter - # Lets Authlogic know about the controller object via a before filter, AKA "activates" authlogic. - module MerbImplementation - def self.included(klass) # :nodoc: - klass.before :activate_authlogic - end - - def cookie_domain - Merb::Config[:session_cookie_domain] - end - - private - def activate_authlogic - Authlogic::Session::Base.controller = MerbAdapter.new(self) - end - end - end - end -end - -# make sure we're running inside Merb -if defined?(Merb::Plugins) - Merb::BootLoader.before_app_loads do - Merb::Controller.send(:include, Authlogic::ControllerAdapters::MerbAdapter::MerbImplementation) - end -end \ No newline at end of file diff --git a/lib/authlogic/controller_adapters/rack_adapter.rb b/lib/authlogic/controller_adapters/rack_adapter.rb new file mode 100644 index 00000000..255deebe --- /dev/null +++ b/lib/authlogic/controller_adapters/rack_adapter.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Authlogic + module ControllerAdapters + # Adapter for authlogic to make it function as a Rack middleware. + # First you'll have write your own Rack adapter where you have to set your cookie domain. + # + # class YourRackAdapter < Authlogic::ControllerAdapters::RackAdapter + # def cookie_domain + # 'your_cookie_domain_here.com' + # end + # end + # + # Next you need to set up a rack middleware like this: + # + # class AuthlogicMiddleware + # def initialize(app) + # @app = app + # end + # + # def call(env) + # YourRackAdapter.new(env) + # @app.call(env) + # end + # end + # + # And that is all! Now just load this middleware into rack: + # + # use AuthlogicMiddleware + # + # Authlogic will expect a User and a UserSession object to be present: + # + # class UserSession < Authlogic::Session::Base + # # Authlogic options go here + # end + # + # class User < ApplicationRecord + # acts_as_authentic + # end + # + class RackAdapter < AbstractAdapter + def initialize(env) + # We use the Rack::Request object as the controller object. + # For this to work, we have to add some glue. + request = Rack::Request.new(env) + + request.instance_eval do + def request + self + end + + def remote_ip + ip + end + end + + super(request) + Authlogic::Session::Base.controller = self + end + + # Rack Requests stores cookies with not just the value, but also with + # flags and expire information in the hash. Authlogic does not like this, + # so we drop everything except the cookie value. + def cookies + controller + .cookies + .map { |key, value_hash| { key => value_hash[:value] } } + .inject(:merge) || {} + end + end + end +end diff --git a/lib/authlogic/controller_adapters/rails_adapter.rb b/lib/authlogic/controller_adapters/rails_adapter.rb index f7b9ac7f..a2f8bc01 100644 --- a/lib/authlogic/controller_adapters/rails_adapter.rb +++ b/lib/authlogic/controller_adapters/rails_adapter.rb @@ -1,48 +1,47 @@ +# frozen_string_literal: true + module Authlogic module ControllerAdapters - # Adapts authlogic to work with rails. The point is to close the gap between what authlogic expects and what the rails controller object - # provides. Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, etc. + # Adapts authlogic to work with rails. The point is to close the gap between + # what authlogic expects and what the rails controller object provides. + # Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, + # etc. class RailsAdapter < AbstractAdapter - class AuthlogicLoadedTooLateError < StandardError; end - def authenticate_with_http_basic(&block) controller.authenticate_with_http_basic(&block) end - + + # Returns a `ActionDispatch::Cookies::CookieJar`. See the AC guide + # http://guides.rubyonrails.org/action_controller_overview.html#cookies def cookies - controller.send(:cookies) + controller.respond_to?(:cookies, true) ? controller.send(:cookies) : nil end - + def cookie_domain - @cookie_domain_key ||= Rails::VERSION::STRING >= '2.3' ? :domain : :session_domain - controller.request.session_options[@cookie_domain_key] + controller.request.session_options[:domain] end - + def request_content_type request.format.to_s end - - # Lets Authlogic know about the controller object via a before filter, AKA "activates" authlogic. + + # Lets Authlogic know about the controller object via a before filter, AKA + # "activates" authlogic. module RailsImplementation def self.included(klass) # :nodoc: - if defined?(::ApplicationController) - raise AuthlogicLoadedTooLateError.new("Authlogic is trying to prepend a before_filter in ActionController::Base to active itself" + - ", the problem is that ApplicationController has already been loaded meaning the before_filter won't get copied into your" + - " application. Generally this is due to another gem or plugin requiring your ApplicationController prematurely, such as" + - " the resource_controller plugin. The solution is to require Authlogic before these other gems / plugins. Please require" + - " authlogic first to get rid of this error.") - end - - klass.prepend_before_filter :activate_authlogic + klass.prepend_before_action :activate_authlogic end - + private - def activate_authlogic - Authlogic::Session::Base.controller = RailsAdapter.new(self) - end + + def activate_authlogic + Authlogic::Session::Base.controller = RailsAdapter.new(self) + end end end end end -ActionController::Base.send(:include, Authlogic::ControllerAdapters::RailsAdapter::RailsImplementation) +ActiveSupport.on_load(:action_controller) do + include Authlogic::ControllerAdapters::RailsAdapter::RailsImplementation +end diff --git a/lib/authlogic/controller_adapters/sinatra_adapter.rb b/lib/authlogic/controller_adapters/sinatra_adapter.rb index 902164c3..4d20c2ae 100644 --- a/lib/authlogic/controller_adapters/sinatra_adapter.rb +++ b/lib/authlogic/controller_adapters/sinatra_adapter.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + # Authlogic bridge for Sinatra module Authlogic module ControllerAdapters module SinatraAdapter + # Cookie management functions class Cookies attr_reader :request, :response @@ -11,7 +14,7 @@ def initialize(request, response) end def delete(key, options = {}) - @request.cookies.delete(key) + @response.delete_cookie(key, options) end def []=(key, options) @@ -23,6 +26,7 @@ def method_missing(meth, *args, &block) end end + # Thin wrapper around request and response. class Controller attr_reader :request, :response, :cookies @@ -32,7 +36,7 @@ def initialize(request, response) end def session - env['rack.session'] + env["rack.session"] end def method_missing(meth, *args, &block) @@ -40,11 +44,13 @@ def method_missing(meth, *args, &block) end end + # Sinatra controller adapter class Adapter < AbstractAdapter def cookie_domain - env['SERVER_NAME'] + env["SERVER_NAME"] end + # Mixed into `Sinatra::Base` module Implementation def self.included(klass) klass.send :before do @@ -58,4 +64,4 @@ def self.included(klass) end end -Sinatra::Request.send(:include, Authlogic::ControllerAdapters::SinatraAdapter::Adapter::Implementation) \ No newline at end of file +Sinatra::Base.send(:include, Authlogic::ControllerAdapters::SinatraAdapter::Adapter::Implementation) diff --git a/lib/authlogic/cookie_credentials.rb b/lib/authlogic/cookie_credentials.rb new file mode 100644 index 00000000..26c692cd --- /dev/null +++ b/lib/authlogic/cookie_credentials.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Authlogic + # Represents the credentials *in* the cookie. The value of the cookie. + # This is primarily a data object. It doesn't interact with controllers. + # It doesn't know about eg. cookie expiration. + # + # @api private + class CookieCredentials + # @api private + class ParseError < RuntimeError + end + + DELIMITER = "::" + + attr_reader :persistence_token, :record_id, :remember_me_until + + # @api private + # @param persistence_token [String] + # @param record_id [String, Numeric] + # @param remember_me_until [ActiveSupport::TimeWithZone] + def initialize(persistence_token, record_id, remember_me_until) + @persistence_token = persistence_token + @record_id = record_id + @remember_me_until = remember_me_until + end + + class << self + # @api private + def parse(string) + parts = string.split(DELIMITER) + unless (1..3).cover?(parts.length) + raise ParseError, format("Expected 1..3 parts, got %d", parts.length) + end + new(parts[0], parts[1], parse_time(parts[2])) + end + + private + + # @api private + def parse_time(string) + return if string.nil? + ::Time.parse(string) + rescue ::ArgumentError => e + raise ParseError, format("Found cookie, cannot parse remember_me_until: #{e}") + end + end + + # @api private + def remember_me? + !@remember_me_until.nil? + end + + # @api private + def to_s + [ + @persistence_token, + @record_id.to_s, + @remember_me_until&.iso8601 + ].compact.join(DELIMITER) + end + end +end diff --git a/lib/authlogic/crypto_providers.rb b/lib/authlogic/crypto_providers.rb new file mode 100644 index 00000000..3e4b4711 --- /dev/null +++ b/lib/authlogic/crypto_providers.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Authlogic + # The acts_as_authentic method has a crypto_provider option. This allows you + # to use any type of encryption you like. Just create a class with a class + # level encrypt and matches? method. See example below. + # + # === Example + # + # class MyAwesomeEncryptionMethod + # def self.encrypt(*tokens) + # # The tokens passed will be an array of objects, what type of object + # # is irrelevant, just do what you need to do with them and return a + # # single encrypted string. For example, you will most likely join all + # # of the objects into a single string and then encrypt that string. + # end + # + # def self.matches?(crypted, *tokens) + # # Return true if the crypted string matches the tokens. Depending on + # # your algorithm you might decrypt the string then compare it to the + # # token, or you might encrypt the tokens and make sure it matches the + # # crypted string, its up to you. + # end + # end + module CryptoProviders + autoload :MD5, "authlogic/crypto_providers/md5" + autoload :Sha1, "authlogic/crypto_providers/sha1" + autoload :Sha256, "authlogic/crypto_providers/sha256" + autoload :Sha512, "authlogic/crypto_providers/sha512" + autoload :BCrypt, "authlogic/crypto_providers/bcrypt" + autoload :SCrypt, "authlogic/crypto_providers/scrypt" + + # Guide users to choose a better crypto provider. + class Guidance + BUILTIN_PROVIDER_PREFIX = "Authlogic::CryptoProviders::" + NONADAPTIVE_ALGORITHM = <<~EOS + You have selected %s as your authlogic crypto provider. This algorithm + does not have any practical known attacks against it. However, there are + better choices. + + Authlogic has no plans yet to deprecate this crypto provider. However, + we recommend transitioning to a more secure, adaptive hashing algorithm, + like scrypt. Adaptive algorithms are designed to slow down brute force + attacks, and over time the iteration count can be increased to make it + slower, so it remains resistant to brute-force search attacks even in + the face of increasing computation power. + + Use the transition_from_crypto_providers option to make the transition + painless for your users. + EOS + VULNERABLE_ALGORITHM = <<~EOS + You have selected %s as your authlogic crypto provider. It is a poor + choice because there are known attacks against this algorithm. + + Authlogic has no plans yet to deprecate this crypto provider. However, + we recommend transitioning to a secure hashing algorithm. We recommend + an adaptive algorithm, like scrypt. + + Use the transition_from_crypto_providers option to make the transition + painless for your users. + EOS + + def initialize(provider) + @provider = provider + end + + def impart_wisdom + return unless @provider.is_a?(Class) + + # We can only impart wisdom about our own built-in providers. + absolute_name = @provider.name + return unless absolute_name.start_with?(BUILTIN_PROVIDER_PREFIX) + + # Inspect the string name of the provider, rather than using the + # constants in our `when` clauses. If we used the constants, we'd + # negate the benefits of the `autoload` above. + name = absolute_name.demodulize + case name + when "MD5", "Sha1" + warn(format(VULNERABLE_ALGORITHM, name)) + when "Sha256", "Sha512" + warn(format(NONADAPTIVE_ALGORITHM, name)) + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/aes256.rb b/lib/authlogic/crypto_providers/aes256.rb deleted file mode 100644 index 40e79709..00000000 --- a/lib/authlogic/crypto_providers/aes256.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "openssl" - -module Authlogic - module CryptoProviders - # This encryption method is reversible if you have the supplied key. So in order to use this encryption method you must supply it with a key first. - # In an initializer, or before your application initializes, you should do the following: - # - # Authlogic::CryptoProviders::AES256.key = "my really long and unique key, preferrably a bunch of random characters" - # - # My final comment is that this is a strong encryption method, but its main weakness is that its reversible. If you do not need to reverse the hash - # then you should consider Sha512 or BCrypt instead. - # - # Keep your key in a safe place, some even say the key should be stored on a separate server. - # This won't hurt performance because the only time it will try and access the key on the separate server is during initialization, which only - # happens once. The reasoning behind this is if someone does compromise your server they won't have the key also. Basically, you don't want to - # store the key with the lock. - class AES256 - class << self - attr_writer :key - - def encrypt(*tokens) - aes.encrypt - aes.key = @key - [aes.update(tokens.join) + aes.final].pack("m").chomp - end - - def matches?(crypted, *tokens) - aes.decrypt - aes.key = @key - (aes.update(crypted.unpack("m").first) + aes.final) == tokens.join - rescue OpenSSL::CipherError - false - end - - private - def aes - raise ArgumentError.new("You must provide a key like #{name}.key = my_key before using the #{name}") if @key.blank? - @aes ||= OpenSSL::Cipher::Cipher.new("AES-256-ECB") - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/crypto_providers/bcrypt.rb b/lib/authlogic/crypto_providers/bcrypt.rb index f2905054..db4aec57 100644 --- a/lib/authlogic/crypto_providers/bcrypt.rb +++ b/lib/authlogic/crypto_providers/bcrypt.rb @@ -1,38 +1,50 @@ -begin - require "bcrypt" -rescue LoadError - "sudo gem install bcrypt-ruby" -end +# frozen_string_literal: true + +require "bcrypt" module Authlogic module CryptoProviders - # For most apps Sha512 is plenty secure, but if you are building an app that stores nuclear launch codes you might want to consier BCrypt. This is an extremely - # secure hashing algorithm, mainly because it is slow. A brute force attack on a BCrypt encrypted password would take much longer than a brute force attack on a - # password encrypted with a Sha algorithm. Keep in mind you are sacrificing performance by using this, generating a password takes exponentially longer than any - # of the Sha algorithms. I did some benchmarking to save you some time with your decision: + # The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) + # is the best choice for password storage today. They have the + # three properties of password hashing that are desirable. They + # are one-way, unique, and slow. While a salted SHA or MD5 hash is + # one-way and unique, preventing rainbow table attacks, they are + # still lightning fast and attacks on the stored passwords are + # much more effective. This benchmark demonstrates the effective + # slowdown that BCrypt provides: # # require "bcrypt" # require "digest" # require "benchmark" # # Benchmark.bm(18) do |x| - # x.report("BCrypt (cost = 10:") { 100.times { BCrypt::Password.create("mypass", :cost => 10) } } - # x.report("BCrypt (cost = 2:") { 100.times { BCrypt::Password.create("mypass", :cost => 2) } } - # x.report("Sha512:") { 100.times { Digest::SHA512.hexdigest("mypass") } } - # x.report("Sha1:") { 100.times { Digest::SHA1.hexdigest("mypass") } } + # x.report("BCrypt (cost = 10:") { + # 100.times { BCrypt::Password.create("mypass", :cost => 10) } + # } + # x.report("BCrypt (cost = 4:") { + # 100.times { BCrypt::Password.create("mypass", :cost => 4) } + # } + # x.report("Sha512:") { + # 100.times { Digest::SHA512.hexdigest("mypass") } + # } + # x.report("Sha1:") { + # 100.times { Digest::SHA1.hexdigest("mypass") } + # } # end # - # user system total real - # BCrypt (cost = 10): 10.780000 0.060000 10.840000 ( 11.100289) - # BCrypt (cost = 2): 0.180000 0.000000 0.180000 ( 0.181914) - # Sha512: 0.000000 0.000000 0.000000 ( 0.000829) - # Sha1: 0.000000 0.000000 0.000000 ( 0.000395) + # user system total real + # BCrypt (cost = 10): 37.360000 0.020000 37.380000 ( 37.558943) + # BCrypt (cost = 4): 0.680000 0.000000 0.680000 ( 0.677460) + # Sha512: 0.000000 0.000000 0.000000 ( 0.000672) + # Sha1: 0.000000 0.000000 0.000000 ( 0.000454) # - # You can play around with the cost to get that perfect balance between performance and security. + # You can play around with the cost to get that perfect balance + # between performance and security. A default cost of 10 is the + # best place to start. # - # Decided BCrypt is for you? Just insall the bcrypt gem: + # Decided BCrypt is for you? Just install the bcrypt gem: # - # gem install bcrypt-ruby + # gem install bcrypt # # Tell acts_as_authentic to use it: # @@ -43,26 +55,38 @@ module CryptoProviders # You are good to go! class BCrypt class << self - # This is the :cost option for the BCrpyt library. The higher the cost the more secure it is and the longer is take the generate a hash. By default this is 10. - # Set this to whatever you want, play around with it to get that perfect balance between security and performance. + # This is the :cost option for the BCrpyt library. The higher the cost + # the more secure it is and the longer is take the generate a hash. By + # default this is 10. Set this to any value >= the engine's minimum + # (currently 4), play around with it to get that perfect balance between + # security and performance. def cost @cost ||= 10 end - attr_writer :cost - + + def cost=(val) + if val < ::BCrypt::Engine::MIN_COST + raise ArgumentError, "Authlogic's bcrypt cost cannot be set below the engine's " \ + "min cost (#{::BCrypt::Engine::MIN_COST})" + end + @cost = val + end + # Creates a BCrypt hash for the password passed. def encrypt(*tokens) - ::BCrypt::Password.create(join_tokens(tokens), :cost => cost) + ::BCrypt::Password.create(join_tokens(tokens), cost: cost) end - - # Does the hash match the tokens? Uses the same tokens that were used to encrypt. + + # Does the hash match the tokens? Uses the same tokens that were used to + # encrypt. def matches?(hash, *tokens) hash = new_from_hash(hash) return false if hash.blank? hash == join_tokens(tokens) end - - # This method is used as a flag to tell Authlogic to "resave" the password upon a successful login, using the new cost + + # This method is used as a flag to tell Authlogic to "resave" the + # password upon a successful login, using the new cost def cost_matches?(hash) hash = new_from_hash(hash) if hash.blank? @@ -71,20 +95,19 @@ def cost_matches?(hash) hash.cost == cost end end - + private - def join_tokens(tokens) - tokens.flatten.join - end - - def new_from_hash(hash) - begin - ::BCrypt::Password.new(hash) - rescue ::BCrypt::Errors::InvalidHash - return nil - end - end + + def join_tokens(tokens) + tokens.flatten.join + end + + def new_from_hash(hash) + ::BCrypt::Password.new(hash) + rescue ::BCrypt::Errors::InvalidHash + nil + end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/crypto_providers/md5.rb b/lib/authlogic/crypto_providers/md5.rb index c3d227ca..6bd5cbfa 100644 --- a/lib/authlogic/crypto_providers/md5.rb +++ b/lib/authlogic/crypto_providers/md5.rb @@ -1,34 +1,36 @@ +# frozen_string_literal: true + require "digest/md5" - + module Authlogic module CryptoProviders - # This class was made for the users transitioning from md5 based systems. - # I highly discourage using this crypto provider as it superbly inferior - # to your other options. - # - # Please use any other provider offered by Authlogic. + # A poor choice. There are known attacks against this algorithm. class MD5 + # V2 hashes the digest bytes in repeated stretches instead of hex characters. + autoload :V2, File.join(__dir__, "md5", "v2") + class << self attr_accessor :join_token - + # The number of times to loop through the encryption. def stretches @stretches ||= 1 end attr_writer :stretches - + # Turns your raw password into a MD5 hash. def encrypt(*tokens) digest = tokens.flatten.join(join_token) stretches.times { digest = Digest::MD5.hexdigest(digest) } digest end - - # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt. + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. def matches?(crypted, *tokens) encrypt(*tokens) == crypted end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/crypto_providers/md5/v2.rb b/lib/authlogic/crypto_providers/md5/v2.rb new file mode 100644 index 00000000..22f28167 --- /dev/null +++ b/lib/authlogic/crypto_providers/md5/v2.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "digest/md5" + +module Authlogic + module CryptoProviders + class MD5 + # A poor choice. There are known attacks against this algorithm. + class V2 + class << self + attr_accessor :join_token + + # The number of times to loop through the encryption. + def stretches + @stretches ||= 1 + end + attr_writer :stretches + + # Turns your raw password into a MD5 hash. + def encrypt(*tokens) + digest = tokens.flatten.join(join_token) + stretches.times { digest = Digest::MD5.digest(digest) } + digest.unpack1("H*") + end + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. + def matches?(crypted, *tokens) + encrypt(*tokens) == crypted + end + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/scrypt.rb b/lib/authlogic/crypto_providers/scrypt.rb new file mode 100644 index 00000000..88ce158e --- /dev/null +++ b/lib/authlogic/crypto_providers/scrypt.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "scrypt" + +module Authlogic + module CryptoProviders + # SCrypt is the default provider for Authlogic. It is the only + # choice in the adaptive hash family that accounts for hardware + # based attacks by compensating with memory bound as well as cpu + # bound computational constraints. It offers the same guarantees + # as BCrypt in the way of one-way, unique and slow. + # + # Decided SCrypt is for you? Just install the scrypt gem: + # + # gem install scrypt + # + # Tell acts_as_authentic to use it: + # + # acts_as_authentic do |c| + # c.crypto_provider = Authlogic::CryptoProviders::SCrypt + # end + class SCrypt + class << self + DEFAULTS = { + key_len: 32, + salt_size: 8, + max_time: 0.2, + max_mem: 1024 * 1024, + max_memfrac: 0.5 + }.freeze + + attr_writer :key_len, :salt_size, :max_time, :max_mem, :max_memfrac + # Key length - length in bytes of generated key, from 16 to 512. + def key_len + @key_len ||= DEFAULTS[:key_len] + end + + # Salt size - size in bytes of random salt, from 8 to 32 + def salt_size + @salt_size ||= DEFAULTS[:salt_size] + end + + # Max time - maximum time spent in computation + def max_time + @max_time ||= DEFAULTS[:max_time] + end + + # Max memory - maximum memory usage. The minimum is always 1MB + def max_mem + @max_mem ||= DEFAULTS[:max_mem] + end + + # Max memory fraction - maximum memory out of all available. Always + # greater than zero and <= 0.5. + def max_memfrac + @max_memfrac ||= DEFAULTS[:max_memfrac] + end + + # Creates an SCrypt hash for the password passed. + def encrypt(*tokens) + ::SCrypt::Password.create( + join_tokens(tokens), + key_len: key_len, + salt_size: salt_size, + max_mem: max_mem, + max_memfrac: max_memfrac, + max_time: max_time + ) + end + + # Does the hash match the tokens? Uses the same tokens that were used to encrypt. + def matches?(hash, *tokens) + hash = new_from_hash(hash) + return false if hash.blank? + hash == join_tokens(tokens) + end + + private + + def join_tokens(tokens) + tokens.flatten.join + end + + def new_from_hash(hash) + ::SCrypt::Password.new(hash) + rescue ::SCrypt::Errors::InvalidHash + nil + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/sha1.rb b/lib/authlogic/crypto_providers/sha1.rb index 819b874a..f992e508 100644 --- a/lib/authlogic/crypto_providers/sha1.rb +++ b/lib/authlogic/crypto_providers/sha1.rb @@ -1,35 +1,42 @@ +# frozen_string_literal: true + require "digest/sha1" module Authlogic module CryptoProviders - # This class was made for the users transitioning from restful_authentication. I highly discourage using this - # crypto provider as it inferior to your other options. Please use any other provider offered by Authlogic. + # A poor choice. There are known attacks against this algorithm. class Sha1 + # V2 hashes the digest bytes in repeated stretches instead of hex characters. + autoload :V2, File.join(__dir__, "sha1", "v2") + class << self def join_token @join_token ||= "--" end attr_writer :join_token - - # The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to. + + # The number of times to loop through the encryption. def stretches @stretches ||= 10 end attr_writer :stretches - + # Turns your raw password into a Sha1 hash. def encrypt(*tokens) tokens = tokens.flatten digest = tokens.shift - stretches.times { digest = Digest::SHA1.hexdigest([digest, *tokens].join(join_token)) } + stretches.times do + digest = Digest::SHA1.hexdigest([digest, *tokens].join(join_token)) + end digest end - - # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt. + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. def matches?(crypted, *tokens) encrypt(*tokens) == crypted end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/crypto_providers/sha1/v2.rb b/lib/authlogic/crypto_providers/sha1/v2.rb new file mode 100644 index 00000000..59c23780 --- /dev/null +++ b/lib/authlogic/crypto_providers/sha1/v2.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "digest/sha1" + +module Authlogic + module CryptoProviders + class Sha1 + # A poor choice. There are known attacks against this algorithm. + class V2 + class << self + def join_token + @join_token ||= "--" + end + attr_writer :join_token + + # The number of times to loop through the encryption. + def stretches + @stretches ||= 10 + end + attr_writer :stretches + + # Turns your raw password into a Sha1 hash. + def encrypt(*tokens) + tokens = tokens.flatten + digest = tokens.shift + stretches.times do + digest = Digest::SHA1.digest([digest, *tokens].join(join_token)) + end + digest.unpack1("H*") + end + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. + def matches?(crypted, *tokens) + encrypt(*tokens) == crypted + end + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/sha256.rb b/lib/authlogic/crypto_providers/sha256.rb index 8bc4077a..c6c7d41d 100644 --- a/lib/authlogic/crypto_providers/sha256.rb +++ b/lib/authlogic/crypto_providers/sha256.rb @@ -1,22 +1,27 @@ +# frozen_string_literal: true + require "digest/sha2" module Authlogic - # The acts_as_authentic method has a crypto_provider option. This allows you to use any type of encryption you like. - # Just create a class with a class level encrypt and matches? method. See example below. + # The acts_as_authentic method has a crypto_provider option. This allows you + # to use any type of encryption you like. Just create a class with a class + # level encrypt and matches? method. See example below. # # === Example # # class MyAwesomeEncryptionMethod # def self.encrypt(*tokens) - # # the tokens passed will be an array of objects, what type of object is irrelevant, - # # just do what you need to do with them and return a single encrypted string. - # # for example, you will most likely join all of the objects into a single string and then encrypt that string + # # the tokens passed will be an array of objects, what type of object + # # is irrelevant, just do what you need to do with them and return a + # # single encrypted string. for example, you will most likely join all + # # of the objects into a single string and then encrypt that string # end # # def self.matches?(crypted, *tokens) - # # return true if the crypted string matches the tokens. - # # depending on your algorithm you might decrypt the string then compare it to the token, or you might - # # encrypt the tokens and make sure it matches the crypted string, its up to you + # # return true if the crypted string matches the tokens. Depending on + # # your algorithm you might decrypt the string then compare it to the + # # token, or you might encrypt the tokens and make sure it matches the + # # crypted string, its up to you. # end # end module CryptoProviders @@ -24,23 +29,27 @@ module CryptoProviders # # Uses the Sha256 hash algorithm to encrypt passwords. class Sha256 + # V2 hashes the digest bytes in repeated stretches instead of hex characters. + autoload :V2, File.join(__dir__, "sha256", "v2") + class << self attr_accessor :join_token - - # The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to. + + # The number of times to loop through the encryption. def stretches @stretches ||= 20 end attr_writer :stretches - + # Turns your raw password into a Sha256 hash. def encrypt(*tokens) digest = tokens.flatten.join(join_token) stretches.times { digest = Digest::SHA256.hexdigest(digest) } digest end - - # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt. + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. def matches?(crypted, *tokens) encrypt(*tokens) == crypted end diff --git a/lib/authlogic/crypto_providers/sha256/v2.rb b/lib/authlogic/crypto_providers/sha256/v2.rb new file mode 100644 index 00000000..5827775e --- /dev/null +++ b/lib/authlogic/crypto_providers/sha256/v2.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "digest/sha2" + +module Authlogic + # The acts_as_authentic method has a crypto_provider option. This allows you + # to use any type of encryption you like. Just create a class with a class + # level encrypt and matches? method. See example below. + # + # === Example + # + # class MyAwesomeEncryptionMethod + # def self.encrypt(*tokens) + # # the tokens passed will be an array of objects, what type of object + # # is irrelevant, just do what you need to do with them and return a + # # single encrypted string. for example, you will most likely join all + # # of the objects into a single string and then encrypt that string + # end + # + # def self.matches?(crypted, *tokens) + # # return true if the crypted string matches the tokens. Depending on + # # your algorithm you might decrypt the string then compare it to the + # # token, or you might encrypt the tokens and make sure it matches the + # # crypted string, its up to you. + # end + # end + module CryptoProviders + class Sha256 + # = Sha256 + # + # Uses the Sha256 hash algorithm to encrypt passwords. + class V2 + class << self + attr_accessor :join_token + + # The number of times to loop through the encryption. + def stretches + @stretches ||= 20 + end + attr_writer :stretches + + # Turns your raw password into a Sha256 hash. + def encrypt(*tokens) + digest = tokens.flatten.join(join_token) + stretches.times { digest = Digest::SHA256.digest(digest) } + digest.unpack1("H*") + end + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. + def matches?(crypted, *tokens) + encrypt(*tokens) == crypted + end + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/sha512.rb b/lib/authlogic/crypto_providers/sha512.rb index 324bd15d..db8093b9 100644 --- a/lib/authlogic/crypto_providers/sha512.rb +++ b/lib/authlogic/crypto_providers/sha512.rb @@ -1,50 +1,38 @@ +# frozen_string_literal: true + require "digest/sha2" module Authlogic - # The acts_as_authentic method has a crypto_provider option. This allows you to use any type of encryption you like. - # Just create a class with a class level encrypt and matches? method. See example below. - # - # === Example - # - # class MyAwesomeEncryptionMethod - # def self.encrypt(*tokens) - # # the tokens passed will be an array of objects, what type of object is irrelevant, - # # just do what you need to do with them and return a single encrypted string. - # # for example, you will most likely join all of the objects into a single string and then encrypt that string - # end - # - # def self.matches?(crypted, *tokens) - # # return true if the crypted string matches the tokens. - # # depending on your algorithm you might decrypt the string then compare it to the token, or you might - # # encrypt the tokens and make sure it matches the crypted string, its up to you - # end - # end module CryptoProviders - # = Sha512 - # - # Uses the Sha512 hash algorithm to encrypt passwords. + # SHA-512 does not have any practical known attacks against it. However, + # there are better choices. We recommend transitioning to a more secure, + # adaptive hashing algorithm, like scrypt. class Sha512 + # V2 hashes the digest bytes in repeated stretches instead of hex characters. + autoload :V2, File.join(__dir__, "sha512", "v2") + class << self attr_accessor :join_token - - # The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to. + + # The number of times to loop through the encryption. def stretches @stretches ||= 20 end attr_writer :stretches - + # Turns your raw password into a Sha512 hash. def encrypt(*tokens) digest = tokens.flatten.join(join_token) stretches.times { digest = Digest::SHA512.hexdigest(digest) } digest end - - # Does the crypted password match the tokens? Uses the same tokens that were used to encrypt. + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. def matches?(crypted, *tokens) encrypt(*tokens) == crypted end end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/crypto_providers/sha512/v2.rb b/lib/authlogic/crypto_providers/sha512/v2.rb new file mode 100644 index 00000000..88df7ccd --- /dev/null +++ b/lib/authlogic/crypto_providers/sha512/v2.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "digest/sha2" + +module Authlogic + module CryptoProviders + class Sha512 + # SHA-512 does not have any practical known attacks against it. However, + # there are better choices. We recommend transitioning to a more secure, + # adaptive hashing algorithm, like scrypt. + class V2 + class << self + attr_accessor :join_token + + # The number of times to loop through the encryption. + def stretches + @stretches ||= 20 + end + attr_writer :stretches + + # Turns your raw password into a Sha512 hash. + def encrypt(*tokens) + digest = tokens.flatten.join(join_token) + stretches.times do + digest = Digest::SHA512.digest(digest) + end + digest.unpack1("H*") + end + + # Does the crypted password match the tokens? Uses the same tokens that + # were used to encrypt. + def matches?(crypted, *tokens) + encrypt(*tokens) == crypted + end + end + end + end + end +end diff --git a/lib/authlogic/crypto_providers/wordpress.rb b/lib/authlogic/crypto_providers/wordpress.rb deleted file mode 100644 index 9974a794..00000000 --- a/lib/authlogic/crypto_providers/wordpress.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'digest/md5' -module Authlogic - module CryptoProviders - class Wordpress - class << self - ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - def matches?(crypted, *tokens) - stretches = 1 << ITOA64.index(crypted[3,1]) - plain, salt = *tokens - hashed = Digest::MD5.digest(salt+plain) - stretches.times do |i| - hashed = Digest::MD5.digest(hashed+plain) - end - crypted[0,12]+encode_64(hashed, 16) == crypted - end - - def encode_64(input, length) - output = "" - i = 0 - while i < length - value = input[i] - i+=1 - break if value.nil? - output += ITOA64[value & 0x3f, 1] - value |= input[i] << 8 if i < length - output += ITOA64[(value >> 6) & 0x3f, 1] - - i+=1 - break if i >= length - value |= input[i] << 16 if i < length - output += ITOA64[(value >> 12) & 0x3f,1] - - i+=1 - break if i >= length - output += ITOA64[(value >> 18) & 0x3f,1] - end - output - end - end - end - end -end diff --git a/lib/authlogic/errors.rb b/lib/authlogic/errors.rb new file mode 100644 index 00000000..7853fa3b --- /dev/null +++ b/lib/authlogic/errors.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Authlogic + # Parent class of all Authlogic errors. + class Error < StandardError + end + + # :nodoc: + class InvalidCryptoProvider < Error + end + + # :nodoc: + class NilCryptoProvider < InvalidCryptoProvider + def message + <<~EOS + In version 5, Authlogic used SCrypt by default. As of version 6, there + is no default. We still recommend SCrypt. If you previously relied on + this default, then, in your User model (or equivalent), please set the + following: + + acts_as_authentic do |c| + c.crypto_provider = ::Authlogic::CryptoProviders::SCrypt + end + + Furthermore, the authlogic gem no longer depends on the scrypt gem. In + your Gemfile, please add scrypt. + + gem "scrypt", "~> 3.0" + + We have made this change in Authlogic 6 so that users of other crypto + providers no longer need to install the scrypt gem. + EOS + end + end + + # :nodoc: + class ModelSetupError < Error + def message + <<-EOS + You must establish a database connection and run the migrations before + using acts_as_authentic. If you need to load the User model before the + database is set up correctly, please set the following: + + acts_as_authentic do |c| + c.raise_on_model_setup_error = false + end + EOS + end + end +end diff --git a/lib/authlogic/i18n.rb b/lib/authlogic/i18n.rb index 575a4336..15bc54b9 100644 --- a/lib/authlogic/i18n.rb +++ b/lib/authlogic/i18n.rb @@ -1,45 +1,60 @@ -require "authlogic/i18n/translator" +# frozen_string_literal: true + +require_relative "i18n/translator" module Authlogic - # This class allows any message in Authlogic to use internationalization. In earlier versions of Authlogic each message was translated via configuration. - # This cluttered up the configuration and cluttered up Authlogic. So all translation has been extracted out into this class. Now all messages pass through - # this class, making it much easier to implement in I18n library / plugin you want. Use this as a layer that sits between Authlogic and whatever I18n - # library you want to use. + # This class allows any message in Authlogic to use internationalization. In + # earlier versions of Authlogic each message was translated via configuration. + # This cluttered up the configuration and cluttered up Authlogic. So all + # translation has been extracted out into this class. Now all messages pass + # through this class, making it much easier to implement in I18n library / + # plugin you want. Use this as a layer that sits between Authlogic and + # whatever I18n library you want to use. # - # By default this uses the rails I18n library, if it exists. If it doesnt exist it just returns the default english message. The Authlogic I18n class - # works EXACTLY like the rails I18n class. This is because the arguments are delegated to this class. + # By default this uses the rails I18n library, if it exists. If it doesn't + # exist it just returns the default English message. The Authlogic I18n class + # works EXACTLY like the rails I18n class. This is because the arguments are + # delegated to this class. # # Here is how all messages are translated internally with Authlogic: # # Authlogic::I18n.t('error_messages.password_invalid', :default => "is invalid") # - # If you use a different I18n library just replace the build-in I18n::Translator class with your own. For example: + # If you use a different I18n library just replace the build-in + # I18n::Translator class with your own. For example: # # class MyAuthlogicI18nTranslator # def translate(key, options = {}) - # # you will have key which will be something like: "error_messages.password_invalid" - # # you will also have options[:default], which will be the default english version of the message + # # you will have key which will be something like: + # # "error_messages.password_invalid" + # # you will also have options[:default], which will be the default + # # English version of the message # # do whatever you want here with the arguments passed to you. # end # end - # + # # Authlogic::I18n.translator = MyAuthlogicI18nTranslator.new # - # That it's! Here is a complete list of the keys that are passed. Just define these however you wish: + # That it's! Here is a complete list of the keys that are passed. Just define + # these however you wish: # # authlogic: # error_messages: # login_blank: can not be blank # login_not_found: is not valid - # login_invalid: should use only letters, numbers, spaces, and .-_@ please. - # consecutive_failed_logins_limit_exceeded: Consecutive failed logins limit exceeded, account is disabled. + # login_invalid: should use only letters, numbers, spaces, and .-_@+ please. + # consecutive_failed_logins_limit_exceeded: > + # Consecutive failed logins limit exceeded, account is disabled. # email_invalid: should look like an email address. + # email_invalid_international: should look like an international email address. # password_blank: can not be blank # password_invalid: is not valid # not_active: Your account is not active # not_confirmed: Your account is not confirmed # not_approved: Your account is not approved # no_authentication_details: You did not provide any details for authentication. + # general_credentials_error: Login/Password combination is not valid + # session_invalid: Your session is invalid and has the following errors: # models: # user_session: UserSession (or whatever name you are using) # attributes: @@ -51,33 +66,35 @@ module Authlogic module I18n @@scope = :authlogic @@translator = nil - + class << self # Returns the current scope. Defaults to :authlogic def scope @@scope end - + # Sets the current scope. Used to set a custom scope. def scope=(scope) @@scope = scope end - + # Returns the current translator. Defaults to +Translator+. def translator @@translator ||= Translator.new end - + # Sets the current translator. Used to set a custom translator. def translator=(translator) @@translator = translator end - - # All message translation is passed to this method. The first argument is the key for the message. The second is options, see the rails I18n library for a list of options used. + + # All message translation is passed to this method. The first argument is + # the key for the message. The second is options, see the rails I18n + # library for a list of options used. def translate(key, options = {}) - translator.translate key, { :scope => I18n.scope }.merge(options) + translator.translate key, { scope: I18n.scope }.merge(options) end - alias :t :translate + alias t translate end end end diff --git a/lib/authlogic/i18n/translator.rb b/lib/authlogic/i18n/translator.rb index 939730e7..170334ec 100644 --- a/lib/authlogic/i18n/translator.rb +++ b/lib/authlogic/i18n/translator.rb @@ -1,11 +1,14 @@ +# frozen_string_literal: true + module Authlogic module I18n + # The default translator used by authlogic/i18n.rb class Translator - # If the I18n gem is present, calls +I18n.translate+ passing all + # If the I18n gem is present, calls +I18n.translate+ passing all # arguments, else returns +options[:default]+. def translate(key, options = {}) if defined?(::I18n) - ::I18n.translate key, options + ::I18n.translate key, **options else options[:default] end diff --git a/lib/authlogic/random.rb b/lib/authlogic/random.rb index 06d8e671..a6acf44b 100644 --- a/lib/authlogic/random.rb +++ b/lib/authlogic/random.rb @@ -1,33 +1,18 @@ +# frozen_string_literal: true + +require "securerandom" + module Authlogic - # Handles generating random strings. If SecureRandom is installed it will default to this and use it instead. SecureRandom comes with ActiveSupport. - # So if you are using this in a rails app you should have this library. + # Generates random strings using ruby's SecureRandom library. module Random - extend self - - SecureRandom = (defined?(::SecureRandom) && ::SecureRandom) || (defined?(::ActiveSupport::SecureRandom) && ::ActiveSupport::SecureRandom) - - if SecureRandom - def hex_token - SecureRandom.hex(64) - end - - def friendly_token - # use base64url as defined by RFC4648 - SecureRandom.base64(15).tr('+/=', '').strip.delete("\n") - end - else - def hex_token - Authlogic::CryptoProviders::Sha512.encrypt(Time.now.to_s + (1..10).collect{ rand.to_s }.join) - end - - FRIENDLY_CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a - - def friendly_token - newpass = "" - 1.upto(20) { |i| newpass << FRIENDLY_CHARS[rand(FRIENDLY_CHARS.size-1)] } - newpass - end + def self.hex_token + SecureRandom.hex(64) + end + + # Returns a string in base64url format as defined by RFC-3548 and RFC-4648. + # We call this a "friendly" token because it is short and safe for URLs. + def self.friendly_token + SecureRandom.urlsafe_base64(15) end - end -end \ No newline at end of file +end diff --git a/lib/authlogic/regex.rb b/lib/authlogic/regex.rb deleted file mode 100644 index 1b751a31..00000000 --- a/lib/authlogic/regex.rb +++ /dev/null @@ -1,25 +0,0 @@ -module Authlogic - # This is a module the contains regular expressions used throughout Authlogic. The point of extracting - # them out into their own module is to make them easily available to you for other uses. Ex: - # - # validates_format_of :my_email_field, :with => Authlogic::Regex.email - module Regex - # A general email regular expression. It allows top level domains (TLD) to be from 2 - 4 in length, any - # TLD longer than that must be manually specified. The decisions behind this regular expression were made - # by reading this website: http://www.regular-expressions.info/email.html, which is an excellent resource - # for regular expressions. - def self.email - return @email_regex if @email_regex - email_name_regex = '[A-Z0-9_\.%\+\-]+' - domain_head_regex = '(?:[A-Z0-9\-]+\.)+' - domain_tld_regex = '(?:[A-Z]{2,4}|museum|travel)' - @email_regex = /^#{email_name_regex}@#{domain_head_regex}#{domain_tld_regex}$/i - end - - # A simple regular expression that only allows for letters, numbers, spaces, and .-_@. Just a standard login / username - # regular expression. - def self.login - /\A\w[\w\.+\-_@ ]+$/ - end - end -end diff --git a/lib/authlogic/session/activation.rb b/lib/authlogic/session/activation.rb deleted file mode 100644 index 0e550de6..00000000 --- a/lib/authlogic/session/activation.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Authlogic - module Session - # Activating Authlogic requires that you pass it an Authlogic::ControllerAdapters::AbstractAdapter object, or a class that extends it. - # This is sort of like a database connection for an ORM library, Authlogic can't do anything until it is "connected" to a controller. - # If you are using a supported framework, Authlogic takes care of this for you. - module Activation - class NotActivatedError < ::StandardError # :nodoc: - def initialize(session) - super("You must activate the Authlogic::Session::Base.controller with a controller object before creating objects") - end - end - - def self.included(klass) - klass.class_eval do - extend ClassMethods - include InstanceMethods - end - end - - module ClassMethods - # Returns true if a controller has been set and can be used properly. This MUST be set before anything can be done. - # Similar to how ActiveRecord won't allow you to do anything without establishing a DB connection. In your framework - # environment this is done for you, but if you are using Authlogic outside of your framework, you need to assign a controller - # object to Authlogic via Authlogic::Session::Base.controller = obj. See the controller= method for more information. - def activated? - !controller.nil? - end - - # This accepts a controller object wrapped with the Authlogic controller adapter. The controller adapters close the gap - # between the different controllers in each framework. That being said, Authlogic is expecting your object's class to - # extend Authlogic::ControllerAdapters::AbstractAdapter. See Authlogic::ControllerAdapters for more info. - # - # Lastly, this is thread safe. - def controller=(value) - Thread.current[:authlogic_controller] = value - end - - # The current controller object - def controller - Thread.current[:authlogic_controller] - end - end - - module InstanceMethods - # Making sure we are activated before we start creating objects - def initialize(*args) - raise NotActivatedError.new(self) unless self.class.activated? - super - end - - private - def controller - self.class.controller - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/active_record_trickery.rb b/lib/authlogic/session/active_record_trickery.rb deleted file mode 100644 index 6a3597c8..00000000 --- a/lib/authlogic/session/active_record_trickery.rb +++ /dev/null @@ -1,64 +0,0 @@ -module Authlogic - module Session - # Authlogic looks like ActiveRecord, sounds like ActiveRecord, but its not ActiveRecord. That's the goal here. - # This is useful for the various rails helper methods such as form_for, error_messages_for, or any method that - # expects an ActiveRecord object. The point is to disguise the object as an ActiveRecord object so we can take - # advantage of the many ActiveRecord tools. - module ActiveRecordTrickery - def self.included(klass) - klass.extend ClassMethods - klass.send(:include, InstanceMethods) - end - - module ClassMethods - # How to name the attributes of Authlogic, works JUST LIKE ActiveRecord, but instead it uses the following - # namespace: - # - # authlogic.attributes.user_session.login - def human_attribute_name(attribute_key_name, options = {}) - options[:count] ||= 1 - options[:default] ||= attribute_key_name.to_s.humanize - I18n.t("attributes.#{name.underscore}.#{attribute_key_name}", options) - end - - # How to name the class, works JUST LIKE ActiveRecord, except it uses the following namespace: - # - # authlogic.models.user_session - def human_name(*args) - I18n.t("models.#{name.underscore}", {:count => 1, :default => name.humanize}) - end - - # For rails < 2.3, mispelled - def self_and_descendents_from_active_record - [self] - end - - # For rails >= 2.3, mispelling fixed - def self_and_descendants_from_active_record - [self] - end - - # For rails >= 3.0 - def model_name - if defined?(::ActiveModel) - ::ActiveModel::Name.new(self) - else - ::ActiveSupport::ModelName.new(self.to_s) - end - end - end - - module InstanceMethods - # Don't use this yourself, this is to just trick some of the helpers since this is the method it calls. - def new_record? - new_session? - end - - # For rails >= 3.0 - def to_model - self - end - end - end - end -end diff --git a/lib/authlogic/session/base.rb b/lib/authlogic/session/base.rb index 10f82b7d..716b658a 100644 --- a/lib/authlogic/session/base.rb +++ b/lib/authlogic/session/base.rb @@ -1,37 +1,2207 @@ +# frozen_string_literal: true + +require "request_store" + module Authlogic - module Session # :nodoc: - # This is the base class Authlogic, where all modules are included. For information on functiionality see the various - # sub modules. + module Session + module Activation + # :nodoc: + class NotActivatedError < ::StandardError + def initialize + super( + "You must activate the Authlogic::Session::Base.controller with " \ + "a controller object before creating objects" + ) + end + end + end + + module Existence + # :nodoc: + class SessionInvalidError < ::StandardError + def initialize(session) + message = I18n.t( + "error_messages.session_invalid", + default: "Your session is invalid and has the following errors:" + ) + message += " #{session.errors.full_messages.to_sentence}" + super message + end + end + end + + # This is the most important class in Authlogic. You will inherit this class + # for your own eg. `UserSession`. + # + # Ongoing consolidation of modules + # ================================ + # + # We are consolidating modules into this class (inlining mixins). When we + # are done, there will only be this one file. It will be quite large, but it + # will be easier to trace execution. + # + # Once consolidation is complete, we hope to identify and extract + # collaborating objects. For example, there may be a "session adapter" that + # connects this class with the existing `ControllerAdapters`. Perhaps a + # data object or a state machine will reveal itself. + # + # Activation + # ========== + # + # Activating Authlogic requires that you pass it an + # Authlogic::ControllerAdapters::AbstractAdapter object, or a class that + # extends it. This is sort of like a database connection for an ORM library, + # Authlogic can't do anything until it is "connected" to a controller. If + # you are using a supported framework, Authlogic takes care of this for you. + # + # ActiveRecord Trickery + # ===================== + # + # Authlogic looks like ActiveRecord, sounds like ActiveRecord, but its not + # ActiveRecord. That's the goal here. This is useful for the various rails + # helper methods such as form_for, error_messages_for, or any method that + # expects an ActiveRecord object. The point is to disguise the object as an + # ActiveRecord object so we can take advantage of the many ActiveRecord + # tools. + # + # Brute Force Protection + # ====================== + # + # A brute force attacks is executed by hammering a login with as many password + # combinations as possible, until one works. A brute force attacked is generally + # combated with a slow hashing algorithm such as BCrypt. You can increase the cost, + # which makes the hash generation slower, and ultimately increases the time it takes + # to execute a brute force attack. Just to put this into perspective, if a hacker was + # to gain access to your server and execute a brute force attack locally, meaning + # there is no network lag, it would probably take decades to complete. Now throw in + # network lag and it would take MUCH longer. + # + # But for those that are extra paranoid and can't get enough protection, why not stop + # them as soon as you realize something isn't right? That's what this module is all + # about. By default the consecutive_failed_logins_limit configuration option is set to + # 50, if someone consecutively fails to login after 50 attempts their account will be + # suspended. This is a very liberal number and at this point it should be obvious that + # something is not right. If you wish to lower this number just set the configuration + # to a lower number: + # + # class UserSession < Authlogic::Session::Base + # consecutive_failed_logins_limit 10 + # end + # + # Callbacks + # ========= + # + # Between these callbacks and the configuration, this is the contract between me and + # you to safely modify Authlogic's behavior. I will do everything I can to make sure + # these do not change. + # + # Check out the sub modules of Authlogic::Session. They are very concise, clear, and + # to the point. More importantly they use the same API that you would use to extend + # Authlogic. That being said, they are great examples of how to extend Authlogic and + # add / modify behavior to Authlogic. These modules could easily be pulled out into + # their own plugin and become an "add on" without any change. + # + # Now to the point of this module. Just like in ActiveRecord you have before_save, + # before_validation, etc. You have similar callbacks with Authlogic, see the METHODS + # constant below. The order of execution is as follows: + # + # before_persisting + # persist + # after_persisting + # [save record if record.has_changes_to_save?] + # + # before_validation + # before_validation_on_create + # before_validation_on_update + # validate + # after_validation_on_update + # after_validation_on_create + # after_validation + # [save record if record.has_changes_to_save?] + # + # before_save + # before_create + # before_update + # after_update + # after_create + # after_save + # [save record if record.has_changes_to_save?] + # + # before_destroy + # [save record if record.has_changes_to_save?] + # after_destroy + # + # Notice the "save record if has_changes_to_save" lines above. This helps with performance. If + # you need to make changes to the associated record, there is no need to save the + # record, Authlogic will do it for you. This allows multiple modules to modify the + # record and execute as few queries as possible. + # + # **WARNING**: unlike ActiveRecord, these callbacks must be set up on the class level: + # + # class UserSession < Authlogic::Session::Base + # before_validation :my_method + # validate :another_method + # # ..etc + # end + # + # You can NOT define a "before_validation" method, this is bad practice and does not + # allow Authlogic to extend properly with multiple extensions. Please ONLY use the + # method above. + # + # HTTP Basic Authentication + # ========================= + # + # Handles all authentication that deals with basic HTTP auth. Which is + # authentication built into the HTTP protocol: + # + # http://username:password@whatever.com + # + # Also, if you are not comfortable letting users pass their raw username and + # password you can use a single access token, as described below. + # + # Magic Columns + # ============= + # + # Just like ActiveRecord has "magic" columns, such as: created_at and updated_at. + # Authlogic has its own "magic" columns too: + # + # * login_count - Increased every time an explicit login is made. This will *NOT* + # increase if logging in by a session, cookie, or basic http auth + # * failed_login_count - This increases for each consecutive failed login. See + # the consecutive_failed_logins_limit option for details. + # * last_request_at - Updates every time the user logs in, either by explicitly + # logging in, or logging in by cookie, session, or http auth + # * current_login_at - Updates with the current time when an explicit login is made. + # * last_login_at - Updates with the value of current_login_at before it is reset. + # * current_login_ip - Updates with the request ip when an explicit login is made. + # * last_login_ip - Updates with the value of current_login_ip before it is reset. + # + # Multiple Simultaneous Sessions + # ============================== + # + # See `id`. Allows you to separate sessions with an id, ultimately letting + # you create multiple sessions for the same user. + # + # Timeout + # ======= + # + # Think about financial websites, if you are inactive for a certain period + # of time you will be asked to log back in on your next request. You can do + # this with Authlogic easily, there are 2 parts to this: + # + # 1. Define the timeout threshold: + # + # acts_as_authentic do |c| + # c.logged_in_timeout = 10.minutes # default is 10.minutes + # end + # + # 2. Enable logging out on timeouts + # + # class UserSession < Authlogic::Session::Base + # logout_on_timeout true # default is false + # end + # + # This will require a user to log back in if they are inactive for more than + # 10 minutes. In order for this feature to be used you must have a + # last_request_at datetime column in your table for whatever model you are + # authenticating with. + # + # Params + # ====== + # + # This module is responsible for authenticating the user via params, which ultimately + # allows the user to log in using a URL like the following: + # + # https://www.domain.com?user_credentials=4LiXF7FiGUppIPubBPey + # + # Notice the token in the URL, this is a single access token. A single access token is + # used for single access only, it is not persisted. Meaning the user provides it, + # Authlogic grants them access, and that's it. If they want access again they need to + # provide the token again. Authlogic will *NEVER* try to persist the session after + # authenticating through this method. + # + # For added security, this token is *ONLY* allowed for RSS and ATOM requests. You can + # change this with the configuration. You can also define if it is allowed dynamically + # by defining a single_access_allowed? method in your controller. For example: + # + # class UsersController < ApplicationController + # private + # def single_access_allowed? + # action_name == "index" + # end + # + # Also, by default, this token is permanent. Meaning if the user changes their + # password, this token will remain the same. It will only change when it is explicitly + # reset. + # + # You can modify all of this behavior with the Config sub module. + # + # Perishable Token + # ================ + # + # Maintains the perishable token, which is helpful for confirming records or + # authorizing records to reset their password. All that this module does is + # reset it after a session have been saved, just keep it changing. The more + # it changes, the tighter the security. + # + # See Authlogic::ActsAsAuthentic::PerishableToken for more information. + # + # Scopes + # ====== + # + # Authentication can be scoped, and it's easy, you just need to define how you want to + # scope everything. See `.with_scope`. + # + # Unauthorized Record + # =================== + # + # Allows you to create session with an object. Ex: + # + # UserSession.create(my_user_object) + # + # Be careful with this, because Authlogic is assuming that you have already + # confirmed that the user is who he says he is. + # + # For example, this is the method used to persist the session internally. + # Authlogic finds the user with the persistence token. At this point we know + # the user is who he says he is, so Authlogic just creates a session with + # the record. This is particularly useful for 3rd party authentication + # methods, such as OpenID. Let that method verify the identity, once it's + # verified, pass the object and create a session. + # + # Magic States + # ============ + # + # Authlogic tries to check the state of the record before creating the session. If + # your record responds to the following methods and any of them return false, + # validation will fail: + # + # Method name Description + # active? Is the record marked as active? + # approved? Has the record been approved? + # confirmed? Has the record been confirmed? + # + # Authlogic does nothing to define these methods for you, its up to you to define what + # they mean. If your object responds to these methods Authlogic will use them, + # otherwise they are ignored. + # + # What's neat about this is that these are checked upon any type of login. When + # logging in explicitly, by cookie, session, or basic http auth. So if you mark a user + # inactive in the middle of their session they wont be logged back in next time they + # refresh the page. Giving you complete control. + # + # Need Authlogic to check your own "state"? No problem, check out the hooks section + # below. Add in a before_validation to do your own checking. The sky is the limit. + # + # Validation + # ========== + # + # The errors in Authlogic work just like ActiveRecord. In fact, it uses + # the `ActiveModel::Errors` class. Use it the same way: + # + # ``` + # class UserSession + # validate :check_if_awesome + # + # private + # + # def check_if_awesome + # if login && !login.include?("awesome") + # errors.add(:login, "must contain awesome") + # end + # unless attempted_record.awesome? + # errors.add(:base, "You must be awesome to log in") + # end + # end + # end + # ``` class Base - include Foundation - include Callbacks - - # Included first so that the session resets itself to nil - include Timeout - - # Included in a specific order so they are tried in this order when persisting - include Params - include Cookies - include Session - include HttpAuth - - # Included in a specific order so magic states gets ran after a record is found - include Password - include UnauthorizedRecord - include MagicStates - - include Activation - include ActiveRecordTrickery - include BruteForceProtection - include Existence - include Klass - include MagicColumns - include PerishableToken - include Persistence - include Scopes - include Id - include Validation - include PriorityRecord + extend ActiveModel::Naming + extend ActiveModel::Translation + extend Authlogic::Config + include ActiveSupport::Callbacks + + E_AC_PARAMETERS = <<~EOS + Passing an ActionController::Parameters to Authlogic is not allowed. + + In Authlogic 3, especially during the transition of rails to Strong + Parameters, it was common for Authlogic users to forget to `permit` + their params. They would pass their params into Authlogic, we'd call + `to_h`, and they'd be surprised when authentication failed. + + In 2018, people are still making this mistake. We'd like to help them + and make authlogic a little simpler at the same time, so in Authlogic + 3.7.0, we deprecated the use of ActionController::Parameters. Instead, + pass a plain Hash. Please replace: + + UserSession.new(user_session_params) + UserSession.create(user_session_params) + + with + + UserSession.new(user_session_params.to_h) + UserSession.create(user_session_params.to_h) + + And don't forget to `permit`! + + We discussed this issue thoroughly between late 2016 and early + 2018. Notable discussions include: + + - https://github.com/binarylogic/authlogic/issues/512 + - https://github.com/binarylogic/authlogic/pull/558 + - https://github.com/binarylogic/authlogic/pull/577 + EOS + E_DPR_FIND_BY_LOGIN_METHOD = <<~EOS.squish.freeze + find_by_login_method is deprecated in favor of record_selection_method, + to avoid confusion with ActiveRecord's "Dynamic Finders". + (https://guides.rubyonrails.org/v6.0/active_record_querying.html#dynamic-finders) + For example, rubocop-rails is confused by the deprecated method. + (https://github.com/rubocop-hq/rubocop-rails/blob/master/lib/rubocop/cop/rails/dynamic_find_by.rb) + EOS + VALID_SAME_SITE_VALUES = [nil, "Lax", "Strict", "None"].freeze + + # Callbacks + # ========= + + METHODS = %w[ + before_persisting + persist + after_persisting + before_validation + before_validation_on_create + before_validation_on_update + validate + after_validation_on_update + after_validation_on_create + after_validation + before_save + before_create + before_update + after_update + after_create + after_save + before_destroy + after_destroy + ].freeze + + # Defines the "callback installation methods" used below. + METHODS.each do |method| + class_eval <<-EOS, __FILE__, __LINE__ + 1 + def self.#{method}(*filter_list, &block) + set_callback(:#{method}, *filter_list, &block) + end + EOS + end + + # Defines session life cycle events that support callbacks. + define_callbacks( + *METHODS, + terminator: ->(_target, result_lambda) { result_lambda.call == false } + ) + define_callbacks( + "persist", + terminator: ->(_target, result_lambda) { result_lambda.call == true } + ) + + # Use the "callback installation methods" defined above + # ----------------------------------------------------- + + before_persisting :reset_stale_state + + # `persist` callbacks, in order of priority + persist :persist_by_params + persist :persist_by_cookie + persist :persist_by_session + persist :persist_by_http_auth, if: :persist_by_http_auth? + + after_persisting :enforce_timeout + after_persisting :update_session, unless: :single_access? + after_persisting :set_last_request_at + + before_save :update_info + before_save :set_last_request_at + + after_save :reset_perishable_token! + after_save :save_cookie, if: :cookie_enabled? + after_save :update_session + after_create :renew_session_id + + after_destroy :destroy_cookie, if: :cookie_enabled? + after_destroy :update_session + + # `validate` callbacks, in deliberate order. For example, + # validate_magic_states must run *after* a record is found. + validate :validate_by_password, if: :authenticating_with_password? + validate( + :validate_by_unauthorized_record, + if: :authenticating_with_unauthorized_record? + ) + validate :validate_magic_states, unless: :disable_magic_states? + validate :reset_failed_login_count, if: :reset_failed_login_count? + validate :validate_failed_logins, if: :being_brute_force_protected? + validate :increase_failed_login_count + + # Accessors + # ========= + + class << self + attr_accessor( + :configured_password_methods + ) + end + attr_accessor( + :invalid_password, + :new_session, + :priority_record, + :record, + :single_access, + :stale_record, + :unauthorized_record + ) + attr_writer( + :scope, + :id + ) + + # Public class methods + # ==================== + + class << self + # Returns true if a controller has been set and can be used properly. + # This MUST be set before anything can be done. Similar to how + # ActiveRecord won't allow you to do anything without establishing a DB + # connection. In your framework environment this is done for you, but if + # you are using Authlogic outside of your framework, you need to assign + # a controller object to Authlogic via + # Authlogic::Session::Base.controller = obj. See the controller= method + # for more information. + def activated? + !controller.nil? + end + + # Allow users to log in via HTTP basic authentication. + # + # * Default: false + # * Accepts: Boolean + def allow_http_basic_auth(value = nil) + rw_config(:allow_http_basic_auth, value, false) + end + alias allow_http_basic_auth= allow_http_basic_auth + + # Lets you change which model to use for authentication. + # + # * Default: inferred from the class name. UserSession would + # automatically try User + # * Accepts: an ActiveRecord class + def authenticate_with(klass) + @klass_name = klass.name + @klass = klass + end + alias authenticate_with= authenticate_with + + # The current controller object + def controller + RequestStore.store[:authlogic_controller] + end + + # This accepts a controller object wrapped with the Authlogic controller + # adapter. The controller adapters close the gap between the different + # controllers in each framework. That being said, Authlogic is expecting + # your object's class to extend + # Authlogic::ControllerAdapters::AbstractAdapter. See + # Authlogic::ControllerAdapters for more info. + # + # Lastly, this is thread safe. + def controller=(value) + RequestStore.store[:authlogic_controller] = value + end + + # To help protect from brute force attacks you can set a limit on the + # allowed number of consecutive failed logins. By default this is 50, + # this is a very liberal number, and if someone fails to login after 50 + # tries it should be pretty obvious that it's a machine trying to login + # in and very likely a brute force attack. + # + # In order to enable this field your model MUST have a + # failed_login_count (integer) field. + # + # If you don't know what a brute force attack is, it's when a machine + # tries to login into a system using every combination of character + # possible. Thus resulting in possibly millions of attempts to log into + # an account. + # + # * Default: 50 + # * Accepts: Integer, set to 0 to disable + def consecutive_failed_logins_limit(value = nil) + rw_config(:consecutive_failed_logins_limit, value, 50) + end + alias consecutive_failed_logins_limit= consecutive_failed_logins_limit + + # The name of the cookie or the key in the cookies hash. Be sure and use + # a unique name. If you have multiple sessions and they use the same + # cookie it will cause problems. Also, if a id is set it will be + # inserted into the beginning of the string. Example: + # + # session = UserSession.new + # session.cookie_key => "user_credentials" + # + # session = UserSession.new(:super_high_secret) + # session.cookie_key => "super_high_secret_user_credentials" + # + # * Default: "#{klass_name.underscore}_credentials" + # * Accepts: String + def cookie_key(value = nil) + rw_config(:cookie_key, value, "#{klass_name.underscore}_credentials") + end + alias cookie_key= cookie_key + + # A convenience method. The same as: + # + # session = UserSession.new(*args) + # session.save + # + # Instead you can do: + # + # UserSession.create(*args) + def create(*args, &block) + session = new(*args) + session.save(&block) + session + end + + # Same as create but calls create!, which raises an exception when + # validation fails. + def create!(*args) + session = new(*args) + session.save! + session + end + + # Set this to true if you want to disable the checking of active?, approved?, and + # confirmed? on your record. This is more or less of a convenience feature, since + # 99% of the time if those methods exist and return false you will not want the + # user logging in. You could easily accomplish this same thing with a + # before_validation method or other callbacks. + # + # * Default: false + # * Accepts: Boolean + def disable_magic_states(value = nil) + rw_config(:disable_magic_states, value, false) + end + alias disable_magic_states= disable_magic_states + + # Once the failed logins limit has been exceed, how long do you want to + # ban the user? This can be a temporary or permanent ban. + # + # * Default: 2.hours + # * Accepts: Fixnum, set to 0 for permanent ban + def failed_login_ban_for(value = nil) + rw_config(:failed_login_ban_for, (!value.nil? && value) || value, 2.hours.to_i) + end + alias failed_login_ban_for= failed_login_ban_for + + # This is how you persist a session. This finds the record for the + # current session using a variety of methods. It basically tries to "log + # in" the user without the user having to explicitly log in. Check out + # the other Authlogic::Session modules for more information. + # + # The best way to use this method is something like: + # + # helper_method :current_user_session, :current_user + # + # def current_user_session + # return @current_user_session if defined?(@current_user_session) + # @current_user_session = UserSession.find + # end + # + # def current_user + # return @current_user if defined?(@current_user) + # @current_user = current_user_session && current_user_session.user + # end + # + # Also, this method accepts a single parameter as the id, to find + # session that you marked with an id: + # + # UserSession.find(:secure) + # + # See the id method for more information on ids. + # + # Priority Record + # =============== + # + # This internal feature supports ActiveRecord's optimistic locking feature, + # which is automatically enabled when a table has a `lock_version` column. + # + # ``` + # # https://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html + # p1 = Person.find(1) + # p2 = Person.find(1) + # p1.first_name = "Michael" + # p1.save + # p2.first_name = "should fail" + # p2.save # Raises an ActiveRecord::StaleObjectError + # ``` + # + # Now, consider the following Authlogic scenario: + # + # ``` + # User.log_in_after_password_change = true + # ben = User.find(1) + # UserSession.create(ben) + # ben.password = "newpasswd" + # ben.password_confirmation = "newpasswd" + # ben.save + # ``` + # + # We've used one of Authlogic's session maintenance features, + # `log_in_after_password_change`. So, when we call `ben.save`, there is a + # `before_save` callback that logs Ben in (`UserSession.find`). Well, when + # we log Ben in, we update his user record, eg. `login_count`. When we're + # done logging Ben in, then the normal `ben.save` happens. So, there were + # two `update` queries. If those two updates came from different User + # instances, we would get a `StaleObjectError`. + # + # Our solution is to carefully pass around a single `User` instance, using + # it for all `update` queries, thus avoiding the `StaleObjectError`. + def find(id = nil, priority_record = nil) + session = new({ priority_record: priority_record }, id) + session.priority_record = priority_record + if session.persisting? + session + end + end + + # @deprecated in favor of record_selection_method + def find_by_login_method(value = nil) + ::ActiveSupport::Deprecation.new.warn(E_DPR_FIND_BY_LOGIN_METHOD) + record_selection_method(value) + end + alias find_by_login_method= find_by_login_method + + # The text used to identify credentials (username/password) combination + # when a bad login attempt occurs. When you show error messages for a + # bad login, it's considered good security practice to hide which field + # the user has entered incorrectly (the login field or the password + # field). For a full explanation, see + # http://www.gnucitizen.org/blog/username-enumeration-vulnerabilities/ + # + # Example of use: + # + # class UserSession < Authlogic::Session::Base + # generalize_credentials_error_messages true + # end + # + # This would make the error message for bad logins and bad passwords + # look identical: + # + # Login/Password combination is not valid + # + # Alternatively you may use a custom message: + # + # class UserSession < AuthLogic::Session::Base + # generalize_credentials_error_messages "Your login information is invalid" + # end + # + # This will instead show your custom error message when the UserSession is invalid. + # + # The downside to enabling this is that is can be too vague for a user + # that has a hard time remembering their username and password + # combinations. It also disables the ability to to highlight the field + # with the error when you use form_for. + # + # If you are developing an app where security is an extreme priority + # (such as a financial application), then you should enable this. + # Otherwise, leaving this off is fine. + # + # * Default false + # * Accepts: Boolean + def generalize_credentials_error_messages(value = nil) + rw_config(:generalize_credentials_error_messages, value, false) + end + alias generalize_credentials_error_messages= generalize_credentials_error_messages + + # HTTP authentication realm + # + # Sets the HTTP authentication realm. + # + # Note: This option has no effect unless request_http_basic_auth is true + # + # * Default: 'Application' + # * Accepts: String + def http_basic_auth_realm(value = nil) + rw_config(:http_basic_auth_realm, value, "Application") + end + alias http_basic_auth_realm= http_basic_auth_realm + + # Should the cookie be set as httponly? If true, the cookie will not be + # accessible from javascript + # + # * Default: true + # * Accepts: Boolean + def httponly(value = nil) + rw_config(:httponly, value, true) + end + alias httponly= httponly + + # How to name the class, works JUST LIKE ActiveRecord, except it uses + # the following namespace: + # + # authlogic.models.user_session + def human_name(*) + I18n.t("models.#{name.underscore}", count: 1, default: name.humanize) + end + + def i18n_scope + I18n.scope + end + + # The name of the class that this session is authenticating with. For + # example, the UserSession class will authenticate with the User class + # unless you specify otherwise in your configuration. See + # authenticate_with for information on how to change this value. + # + # @api public + def klass + @klass ||= klass_name ? klass_name.constantize : nil + end + + # The model name, guessed from the session class name, e.g. "User", + # from "UserSession". + # + # TODO: This method can return nil. We should explore this. It seems + # likely to cause a NoMethodError later, so perhaps we should raise an + # error instead. + # + # @api private + def klass_name + return @klass_name if instance_variable_defined?(:@klass_name) + @klass_name = name.scan(/(.*)Session/)[0]&.first + end + + # The name of the method you want Authlogic to create for storing the + # login / username. Keep in mind this is just for your + # Authlogic::Session, if you want it can be something completely + # different than the field in your model. So if you wanted people to + # login with a field called "login" and then find users by email this is + # completely doable. See the `record_selection_method` configuration + # option for details. + # + # * Default: klass.login_field || klass.email_field + # * Accepts: Symbol or String + def login_field(value = nil) + rw_config(:login_field, value, klass.login_field || klass.email_field) + end + alias login_field= login_field + + # With acts_as_authentic you get a :logged_in_timeout configuration + # option. If this is set, after this amount of time has passed the user + # will be marked as logged out. Obviously, since web based apps are on a + # per request basis, we have to define a time limit threshold that + # determines when we consider a user to be "logged out". Meaning, if + # they login and then leave the website, when do mark them as logged + # out? I recommend just using this as a fun feature on your website or + # reports, giving you a ballpark number of users logged in and active. + # This is not meant to be a dead accurate representation of a user's + # logged in state, since there is really no real way to do this with web + # based apps. Think about a user that logs in and doesn't log out. There + # is no action that tells you that the user isn't technically still + # logged in and active. + # + # That being said, you can use that feature to require a new login if + # their session times out. Similar to how financial sites work. Just set + # this option to true and if your record returns true for stale? then + # they will be required to log back in. + # + # Lastly, UserSession.find will still return an object if the session is + # stale, but you will not get a record. This allows you to determine if + # the user needs to log back in because their session went stale, or + # because they just aren't logged in. Just call + # current_user_session.stale? as your flag. + # + # * Default: false + # * Accepts: Boolean + def logout_on_timeout(value = nil) + rw_config(:logout_on_timeout, value, false) + end + alias logout_on_timeout= logout_on_timeout + + # Every time a session is found the last_request_at field for that record is + # updated with the current time, if that field exists. If you want to limit how + # frequent that field is updated specify the threshold here. For example, if your + # user is making a request every 5 seconds, and you feel this is too frequent, and + # feel a minute is a good threshold. Set this to 1.minute. Once a minute has + # passed in between requests the field will be updated. + # + # * Default: 0 + # * Accepts: integer representing time in seconds + def last_request_at_threshold(value = nil) + rw_config(:last_request_at_threshold, value, 0) + end + alias last_request_at_threshold= last_request_at_threshold + + # Works exactly like cookie_key, but for params. So a user can login via + # params just like a cookie or a session. Your URL would look like: + # + # http://www.domain.com?user_credentials=my_single_access_key + # + # You can change the "user_credentials" key above with this + # configuration option. Keep in mind, just like cookie_key, if you + # supply an id the id will be appended to the front. Check out + # cookie_key for more details. Also checkout the "Single Access / + # Private Feeds Access" section in the README. + # + # * Default: cookie_key + # * Accepts: String + def params_key(value = nil) + rw_config(:params_key, value, cookie_key) + end + alias params_key= params_key + + # Works exactly like login_field, but for the password instead. Returns + # :password if a login_field exists. + # + # * Default: :password + # * Accepts: Symbol or String + def password_field(value = nil) + rw_config(:password_field, value, login_field && :password) + end + alias password_field= password_field + + # Authlogic tries to validate the credentials passed to it. One part of + # validation is actually finding the user and making sure it exists. + # What method it uses the do this is up to you. + # + # ``` + # # user_session.rb + # record_selection_method :find_by_email + # ``` + # + # This is the recommended way to find the user by email address. + # The resulting query will be `User.find_by_email(send(login_field))`. + # (`login_field` will fall back to `email_field` if there's no `login` + # or `username` column). + # + # In your User model you can make that method do anything you want, + # giving you complete control of how users are found by the UserSession. + # + # Let's take an example: You want to allow users to login by username or + # email. Set this to the name of the class method that does this in the + # User model. Let's call it "find_by_username_or_email" + # + # ``` + # class User < ActiveRecord::Base + # def self.find_by_username_or_email(login) + # find_by_username(login) || find_by_email(login) + # end + # end + # ``` + # + # Now just specify the name of this method for this configuration option + # and you are all set. You can do anything you want here. Maybe you + # allow users to have multiple logins and you want to search a has_many + # relationship, etc. The sky is the limit. + # + # * Default: "find_by_smart_case_login_field" + # * Accepts: Symbol or String + def record_selection_method(value = nil) + rw_config(:record_selection_method, value, "find_by_smart_case_login_field") + end + alias record_selection_method= record_selection_method + + # Whether or not to request HTTP authentication + # + # If set to true and no HTTP authentication credentials are sent with + # the request, the Rails controller method + # authenticate_or_request_with_http_basic will be used and a '401 + # Authorization Required' header will be sent with the response. In + # most cases, this will cause the classic HTTP authentication popup to + # appear in the users browser. + # + # If set to false, the Rails controller method + # authenticate_with_http_basic is used and no 401 header is sent. + # + # Note: This parameter has no effect unless allow_http_basic_auth is + # true + # + # * Default: false + # * Accepts: Boolean + def request_http_basic_auth(value = nil) + rw_config(:request_http_basic_auth, value, false) + end + alias request_http_basic_auth= request_http_basic_auth + + # If sessions should be remembered by default or not. + # + # * Default: false + # * Accepts: Boolean + def remember_me(value = nil) + rw_config(:remember_me, value, false) + end + alias remember_me= remember_me + + # The length of time until the cookie expires. + # + # * Default: 3.months + # * Accepts: Integer, length of time in seconds, such as 60 or 3.months + def remember_me_for(value = nil) + rw_config(:remember_me_for, value, 3.months) + end + alias remember_me_for= remember_me_for + + # Should the cookie be prevented from being send along with cross-site + # requests? + # + # * Default: nil + # * Accepts: String, one of nil, 'Lax' or 'Strict' + def same_site(value = nil) + unless VALID_SAME_SITE_VALUES.include?(value) + msg = "Invalid same_site value: #{value}. Valid: #{VALID_SAME_SITE_VALUES.inspect}" + raise ArgumentError, msg + end + rw_config(:same_site, value) + end + alias same_site= same_site + + # The current scope set, should be used in the block passed to with_scope. + def scope + RequestStore.store[:authlogic_scope] + end + + # Should the cookie be set as secure? If true, the cookie will only be sent over + # SSL connections + # + # * Default: true + # * Accepts: Boolean + def secure(value = nil) + rw_config(:secure, value, true) + end + alias secure= secure + + # Should the Rack session ID be reset after authentication, to protect + # against Session Fixation attacks? + # + # * Default: true + # * Accepts: Boolean + def session_fixation_defense(value = nil) + rw_config(:session_fixation_defense, value, true) + end + alias session_fixation_defense= session_fixation_defense + + # Should the cookie be signed? If the controller adapter supports it, this is a + # measure against cookie tampering. + def sign_cookie(value = nil) + if value && controller && !controller.cookies.respond_to?(:signed) + raise "Signed cookies not supported with #{controller.class}!" + end + rw_config(:sign_cookie, value, false) + end + alias sign_cookie= sign_cookie + + # Should the cookie be encrypted? If the controller adapter supports it, this is a + # measure to hide the contents of the cookie (e.g. persistence_token) + def encrypt_cookie(value = nil) + if value && controller && !controller.cookies.respond_to?(:encrypted) + raise "Encrypted cookies not supported with #{controller.class}!" + end + if value && sign_cookie + raise "It is recommended to use encrypt_cookie instead of sign_cookie. " \ + "You may not enable both options." + end + rw_config(:encrypt_cookie, value, false) + end + alias encrypt_cookie= encrypt_cookie + + # Works exactly like cookie_key, but for sessions. See cookie_key for more info. + # + # * Default: cookie_key + # * Accepts: Symbol or String + def session_key(value = nil) + rw_config(:session_key, value, cookie_key) + end + alias session_key= session_key + + # Authentication is allowed via a single access token, but maybe this is + # something you don't want for your application as a whole. Maybe this + # is something you only want for specific request types. Specify a list + # of allowed request types and single access authentication will only be + # allowed for the ones you specify. + # + # * Default: ["application/rss+xml", "application/atom+xml"] + # * Accepts: String of a request type, or :all or :any to + # allow single access authentication for any and all request types + def single_access_allowed_request_types(value = nil) + rw_config( + :single_access_allowed_request_types, + value, + ["application/rss+xml", "application/atom+xml"] + ) + end + alias single_access_allowed_request_types= single_access_allowed_request_types + + # The name of the method in your model used to verify the password. This + # should be an instance method. It should also be prepared to accept a + # raw password and a crytped password. + # + # * Default: "valid_password?" defined in acts_as_authentic/password.rb + # * Accepts: Symbol or String + def verify_password_method(value = nil) + rw_config(:verify_password_method, value, "valid_password?") + end + alias verify_password_method= verify_password_method + + # What with_scopes focuses on is scoping the query when finding the + # object and the name of the cookie / session. It works very similar to + # ActiveRecord::Base#with_scopes. It accepts a hash with any of the + # following options: + # + # * find_options: any options you can pass into ActiveRecord::Base.find. + # This is used when trying to find the record. + # * id: The id of the session, this gets merged with the real id. For + # information ids see the id method. + # + # Here is how you use it: + # + # ``` + # UserSession.with_scope(find_options: User.where(account_id: 2), id: "account_2") do + # UserSession.find + # end + # ``` + # + # Essentially what the above does is scope the searching of the object + # with the sql you provided. So instead of: + # + # ``` + # User.where("login = 'ben'").first + # ``` + # + # it would effectively be: + # + # ``` + # User.where("login = 'ben' and account_id = 2").first + # ``` + # + # You will also notice the :id option. This works just like the id + # method. It scopes your cookies. So the name of your cookie will be: + # + # account_2_user_credentials + # + # instead of: + # + # user_credentials + # + # What is also nifty about scoping with an :id is that it merges your + # id's. So if you do: + # + # UserSession.with_scope( + # find_options: { conditions: "account_id = 2"}, + # id: "account_2" + # ) do + # session = UserSession.new + # session.id = :secure + # end + # + # The name of your cookies will be: + # + # secure_account_2_user_credentials + def with_scope(options = {}) + raise ArgumentError, "You must provide a block" unless block_given? + self.scope = options + result = yield + self.scope = nil + result + end + end + + # Constructor + # =========== + + def initialize(*args) + @id = nil + self.scope = self.class.scope + define_record_alias_method + raise Activation::NotActivatedError unless self.class.activated? + unless self.class.configured_password_methods + configure_password_methods + self.class.configured_password_methods = true + end + instance_variable_set("@#{password_field}", nil) + self.credentials = args + end + + # Public instance methods + # ======================= + + # You should use this as a place holder for any records that you find + # during validation. The main reason for this is to allow other modules to + # use it if needed. Take the failed_login_count feature, it needs this in + # order to increase the failed login count. + def attempted_record + @attempted_record + end + + # See attempted_record + def attempted_record=(value) + value = priority_record if value == priority_record # See notes in `.find` + @attempted_record = value + end + + # Returns true when the consecutive_failed_logins_limit has been + # exceeded and is being temporarily banned. Notice the word temporary, + # the user will not be permanently banned unless you choose to do so + # with configuration. By default they will be banned for 2 hours. During + # that 2 hour period this method will return true. + def being_brute_force_protected? + exceeded_failed_logins_limit? && + ( + failed_login_ban_for <= 0 || + attempted_record.respond_to?(:updated_at) && + attempted_record.updated_at >= failed_login_ban_for.seconds.ago + ) + end + + # The credentials you passed to create your session, in a redacted format + # intended for output (debugging, logging). See credentials= for more + # info. + # + # @api private + def credentials + if authenticating_with_unauthorized_record? + { unauthorized_record: "" } + elsif authenticating_with_password? + { + login_field.to_sym => send(login_field), + password_field.to_sym => "" + } + else + {} + end + end + + # Set your credentials before you save your session. There are many + # method signatures. + # + # ``` + # # A hash of credentials is most common + # session.credentials = { login: "foo", password: "bar", remember_me: true } + # + # # You must pass an actual Hash, `ActionController::Parameters` is + # # specifically not allowed. + # + # # You can pass an array of objects: + # session.credentials = [my_user_object, true] + # + # # If you need to set an id (see `#id`) pass it last. + # session.credentials = [ + # {:login => "foo", :password => "bar", :remember_me => true}, + # :my_id + # ] + # session.credentials = [my_user_object, true, :my_id] + # + # The `id` is something that you control yourself, it should never be + # set from a hash or a form. + # + # # Finally, there's priority_record + # [{ priority_record: my_object }, :my_id] + # ``` + # + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def credentials=(value) + normalized = Array.wrap(value) + if normalized.first.class.name == "ActionController::Parameters" + raise TypeError, E_AC_PARAMETERS + end + + # Allows you to set the remember_me option when passing credentials. + values = value.is_a?(Array) ? value : [value] + case values.first + when Hash + if values.first.with_indifferent_access.key?(:remember_me) + self.remember_me = values.first.with_indifferent_access[:remember_me] + end + else + r = values.find { |val| val.is_a?(TrueClass) || val.is_a?(FalseClass) } + self.remember_me = r unless r.nil? + end + + # Accepts the login_field / password_field credentials combination in + # hash form. + # + # You must pass an actual Hash, `ActionController::Parameters` is + # specifically not allowed. + values = Array.wrap(value) + if values.first.is_a?(Hash) + sliced = values + .first + .with_indifferent_access + .slice(login_field, password_field) + sliced.each do |field, val| + next if val.blank? + send("#{field}=", val) + end + end + + # Setting the unauthorized record if it exists in the credentials passed. + values = value.is_a?(Array) ? value : [value] + self.unauthorized_record = values.first if values.first.class < ::ActiveRecord::Base + + # Setting the id if it is passed in the credentials. + values = value.is_a?(Array) ? value : [value] + self.id = values.last if values.last.is_a?(Symbol) + + # Setting priority record if it is passed. The only way it can be passed + # is through an array: + # + # session.credentials = [real_user_object, priority_user_object] + # + # See notes in `.find` + values = value.is_a?(Array) ? value : [value] + self.priority_record = values[1] if values[1].class < ::ActiveRecord::Base + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + # Clears all errors and the associated record, you should call this + # terminate a session, thus requiring the user to authenticate again if + # it is needed. + def destroy + run_callbacks :before_destroy + save_record + errors.clear + @record = nil + run_callbacks :after_destroy + true + end + + def destroyed? + record.nil? + end + + # @api public + def errors + @errors ||= ::ActiveModel::Errors.new(self) + end + + # If the cookie should be marked as httponly (not accessible via javascript) + def httponly + return @httponly if defined?(@httponly) + @httponly = self.class.httponly + end + + # Accepts a boolean as to whether the cookie should be marked as + # httponly. If true, the cookie will not be accessible from javascript + def httponly=(value) + @httponly = value + end + + # See httponly + def httponly? + httponly == true || httponly == "true" || httponly == "1" + end + + # Allows you to set a unique identifier for your session, so that you can + # have more than 1 session at a time. + # + # For example, you may want to have simultaneous private and public + # sessions. Or, a normal user session and a "secure" user session. The + # secure user session would be created only when they want to modify their + # billing information, or other sensitive information. + # + # You can set the id during initialization (see initialize for more + # information), or as an attribute: + # + # session.id = :my_id + # + # Set your id before you save your session. + # + # Lastly, to retrieve your session with the id, use the `.find` method. + def id + @id + end + + def inspect + format( + "#<%s: %s>", + self.class.name, + credentials.blank? ? "no credentials provided" : credentials.inspect + ) + end + + def invalid_password? + invalid_password == true + end + + # Don't use this yourself, this is to just trick some of the helpers + # since this is the method it calls. + def new_record? + new_session? + end + + # Returns true if the session is new, meaning no action has been taken + # on it and a successful save has not taken place. + def new_session? + new_session != false + end + + def persisted? + !(new_record? || destroyed?) + end + + # Returns boolean indicating if the session is being persisted or not, + # meaning the user does not have to explicitly log in in order to be + # logged in. + # + # If the session has no associated record, it will try to find a record + # and persist the session. + # + # This is the method that the class level method find uses to ultimately + # persist the session. + def persisting? + return true unless record.nil? + self.attempted_record = nil + self.remember_me = cookie_credentials&.remember_me? + run_callbacks :before_persisting + run_callbacks :persist + ensure_authentication_attempted + if errors.empty? && !attempted_record.nil? + self.record = attempted_record + run_callbacks :after_persisting + save_record + self.new_session = false + true + else + false + end + end + + def save_record(alternate_record = nil) + r = alternate_record || record + if r != priority_record + if r&.has_changes_to_save? && !r.readonly? + r.save_without_session_maintenance(validate: false) + end + end + end + + # Tells you if the record is stale or not. Meaning the record has timed + # out. This will only return true if you set logout_on_timeout to true + # in your configuration. Basically how a bank website works. If you + # aren't active over a certain period of time your session becomes stale + # and requires you to log back in. + def stale? + if remember_me? + remember_me_expired? + else + !stale_record.nil? || (logout_on_timeout? && record && record.logged_out?) + end + end + + # Is the cookie going to expire after the session is over, or will it stick around? + def remember_me + return @remember_me if defined?(@remember_me) + @remember_me = self.class.remember_me + end + + # Accepts a boolean as a flag to remember the session or not. Basically + # to expire the cookie at the end of the session or keep it for + # "remember_me_until". + def remember_me=(value) + @remember_me = value + end + + # See remember_me + def remember_me? + remember_me == true || remember_me == "true" || remember_me == "1" + end + + # Has the cookie expired due to current time being greater than remember_me_until. + def remember_me_expired? + return unless remember_me? + cookie_credentials.remember_me_until < ::Time.now + end + + # How long to remember the user if remember_me is true. This is based on the class + # level configuration: remember_me_for + def remember_me_for + return unless remember_me? + self.class.remember_me_for + end + + # When to expire the cookie. See remember_me_for configuration option to change + # this. + def remember_me_until + return unless remember_me? + remember_me_for.from_now + end + + # After you have specified all of the details for your session you can + # try to save it. This will run validation checks and find the + # associated record, if all validation passes. If validation does not + # pass, the save will fail and the errors will be stored in the errors + # object. + def save + result = nil + if valid? + self.record = attempted_record + + run_callbacks :before_save + run_callbacks(new_session? ? :before_create : :before_update) + run_callbacks(new_session? ? :after_create : :after_update) + run_callbacks :after_save + + save_record + self.new_session = false + result = true + else + result = false + end + + yield result if block_given? + result + end + + # Same as save but raises an exception of validation errors when + # validation fails + def save! + result = save + raise Existence::SessionInvalidError, self unless result + result + end + + # If the cookie should be marked as secure (SSL only) + def secure + return @secure if defined?(@secure) + @secure = self.class.secure + end + + # Accepts a boolean as to whether the cookie should be marked as secure. If true + # the cookie will only ever be sent over an SSL connection. + def secure=(value) + @secure = value + end + + # See secure + def secure? + secure == true || secure == "true" || secure == "1" + end + + # If the cookie should be marked as SameSite with 'Lax' or 'Strict' flag. + def same_site + return @same_site if defined?(@same_site) + @same_site = self.class.same_site(nil) + end + + # Accepts nil, 'Lax' or 'Strict' as possible flags. + def same_site=(value) + unless VALID_SAME_SITE_VALUES.include?(value) + msg = "Invalid same_site value: #{value}. Valid: #{VALID_SAME_SITE_VALUES.inspect}" + raise ArgumentError, msg + end + @same_site = value + end + + # If the cookie should be signed + def sign_cookie + return @sign_cookie if defined?(@sign_cookie) + @sign_cookie = self.class.sign_cookie + end + + # Accepts a boolean as to whether the cookie should be signed. If true + # the cookie will be saved and verified using a signature. + def sign_cookie=(value) + @sign_cookie = value + end + + # See sign_cookie + def sign_cookie? + sign_cookie == true || sign_cookie == "true" || sign_cookie == "1" + end + + # If the cookie should be encrypted + def encrypt_cookie + return @encrypt_cookie if defined?(@encrypt_cookie) + @encrypt_cookie = self.class.encrypt_cookie + end + + # Accepts a boolean as to whether the cookie should be encrypted. If true + # the cookie will be saved in an encrypted state. + def encrypt_cookie=(value) + @encrypt_cookie = value + end + + # See encrypt_cookie + def encrypt_cookie? + encrypt_cookie == true || encrypt_cookie == "true" || encrypt_cookie == "1" + end + + # The scope of the current object + def scope + @scope ||= {} + end + + def to_key + new_record? ? nil : record.to_key + end + + # For rails >= 3.0 + def to_model + self + end + + # Determines if the information you provided for authentication is valid + # or not. If there is a problem with the information provided errors will + # be added to the errors object and this method will return false. + # + # @api public + def valid? + errors.clear + self.attempted_record = nil + run_the_before_validation_callbacks + + # Run the `validate` callbacks, eg. `validate_by_password`. + # This is when `attempted_record` is set. + run_callbacks(:validate) + + ensure_authentication_attempted + if errors.empty? + run_the_after_validation_callbacks + end + save_record(attempted_record) + errors.empty? + end + + # Private class methods + # ===================== + + class << self + private + + def scope=(value) + RequestStore.store[:authlogic_scope] = value + end + end + + # Private instance methods + # ======================== + + private + + def add_general_credentials_error + error_message = + if self.class.generalize_credentials_error_messages.is_a? String + self.class.generalize_credentials_error_messages + else + "#{login_field.to_s.humanize}/Password combination is not valid" + end + errors.add( + :base, + I18n.t("error_messages.general_credentials_error", default: error_message) + ) + end + + def add_invalid_password_error + if generalize_credentials_error_messages? + add_general_credentials_error + else + errors.add( + password_field, + I18n.t("error_messages.password_invalid", default: "is not valid") + ) + end + end + + def add_login_not_found_error + if generalize_credentials_error_messages? + add_general_credentials_error + else + errors.add( + login_field, + I18n.t("error_messages.login_not_found", default: "is not valid") + ) + end + end + + def allow_http_basic_auth? + self.class.allow_http_basic_auth == true + end + + def authenticating_with_password? + login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?) + end + + def authenticating_with_unauthorized_record? + !unauthorized_record.nil? + end + + # Used for things like cookie_key, session_key, etc. + # Examples: + # - user_credentials + # - ziggity_zack_user_credentials + # - ziggity_zack is an "id" + # - see persistence_token_test.rb + def build_key(last_part) + [id, scope[:id], last_part].compact.join("_") + end + + def clear_failed_login_count + if record.respond_to?(:failed_login_count) + record.failed_login_count = 0 + end + end + + def consecutive_failed_logins_limit + self.class.consecutive_failed_logins_limit + end + + def controller + self.class.controller + end + + def cookie_key + build_key(self.class.cookie_key) + end + + # Look in the `cookie_jar`, find the cookie that contains authlogic + # credentials (`cookie_key`). + # + # @api private + # @return ::Authlogic::CookieCredentials or if no cookie is found, nil + def cookie_credentials + return unless cookie_enabled? + + cookie_value = cookie_jar[cookie_key] + unless cookie_value.nil? + ::Authlogic::CookieCredentials.parse(cookie_value) + end + end + + def cookie_enabled? + !controller.cookies.nil? + end + + def cookie_jar + if self.class.encrypt_cookie + controller.cookies.encrypted + elsif self.class.sign_cookie + controller.cookies.signed + else + controller.cookies + end + end + + def configure_password_methods + define_login_field_methods + define_password_field_methods + end + + # Assign a new controller-session ID, to defend against Session Fixation. + # https://guides.rubyonrails.org/v6.0/security.html#session-fixation + def renew_session_id + return unless self.class.session_fixation_defense + controller.renew_session_id + end + + def define_login_field_methods + return unless login_field + self.class.send(:attr_writer, login_field) unless respond_to?("#{login_field}=") + self.class.send(:attr_reader, login_field) unless respond_to?(login_field) + end + + # @api private + def define_password_field_methods + return unless password_field + define_password_field_writer_method + define_password_field_reader_methods + end + + # The password should not be accessible publicly. This way forms using + # form_for don't fill the password with the attempted password. To prevent + # this we just create this method that is private. + # + # @api private + def define_password_field_reader_methods + unless respond_to?(password_field) + # Deliberate no-op method, see rationale above. + self.class.send(:define_method, password_field) {} + end + self.class.class_eval( + <<-EOS, __FILE__, __LINE__ + 1 + private + def protected_#{password_field} + @#{password_field} + end + EOS + ) + end + + def define_password_field_writer_method + unless respond_to?("#{password_field}=") + self.class.send(:attr_writer, password_field) + end + end + + # Creating an alias method for the "record" method based on the klass + # name, so that we can do: + # + # session.user + # + # instead of: + # + # session.record + # + # @api private + def define_record_alias_method + noun = klass_name.demodulize.underscore.to_sym + return if respond_to?(noun) + self.class.send(:alias_method, noun, :record) + end + + def destroy_cookie + controller.cookies.delete cookie_key, domain: controller.cookie_domain + end + + def disable_magic_states? + self.class.disable_magic_states == true + end + + def enforce_timeout + if stale? + self.stale_record = record + self.record = nil + end + end + + def ensure_authentication_attempted + if errors.empty? && attempted_record.nil? + errors.add( + :base, + I18n.t( + "error_messages.no_authentication_details", + default: "You did not provide any details for authentication." + ) + ) + end + end + + def exceeded_failed_logins_limit? + !attempted_record.nil? && + attempted_record.respond_to?(:failed_login_count) && + consecutive_failed_logins_limit > 0 && + attempted_record.failed_login_count && + attempted_record.failed_login_count >= consecutive_failed_logins_limit + end + + # @deprecated in favor of `self.class.record_selection_method` + def find_by_login_method + ::ActiveSupport::Deprecation.new.warn(E_DPR_FIND_BY_LOGIN_METHOD) + self.class.record_selection_method + end + + def generalize_credentials_error_messages? + self.class.generalize_credentials_error_messages + end + + # @api private + def generate_cookie_for_saving + { + value: generate_cookie_value.to_s, + expires: remember_me_until, + secure: secure, + httponly: httponly, + same_site: same_site, + domain: controller.cookie_domain + } + end + + def generate_cookie_value + ::Authlogic::CookieCredentials.new( + record.persistence_token, + record.send(record.class.primary_key), + remember_me? ? remember_me_until : nil + ) + end + + # Returns a Proc to be executed by + # `ActionController::HttpAuthentication::Basic` when credentials are + # present in the HTTP request. + # + # @api private + # @return Proc + def http_auth_login_proc + proc do |login, password| + if !login.blank? && !password.blank? + send("#{login_field}=", login) + send("#{password_field}=", password) + valid? + end + end + end + + def failed_login_ban_for + self.class.failed_login_ban_for + end + + def increase_failed_login_count + if invalid_password? && attempted_record.respond_to?(:failed_login_count) + attempted_record.failed_login_count ||= 0 + attempted_record.failed_login_count += 1 + end + end + + def increment_login_count + if record.respond_to?(:login_count) + record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1) + end + end + + def klass + self.class.klass + end + + def klass_name + self.class.klass_name + end + + def last_request_at_threshold + self.class.last_request_at_threshold + end + + def login_field + self.class.login_field + end + + def logout_on_timeout? + self.class.logout_on_timeout == true + end + + def params_credentials + controller.params[params_key] + end + + def params_enabled? + if !params_credentials || !klass.column_names.include?("single_access_token") + return false + end + if controller.responds_to_single_access_allowed? + return controller.single_access_allowed? + end + params_enabled_by_allowed_request_types? + end + + def params_enabled_by_allowed_request_types? + case single_access_allowed_request_types + when Array + single_access_allowed_request_types.include?(controller.request_content_type) || + single_access_allowed_request_types.include?(:all) + else + %i[all any].include?(single_access_allowed_request_types) + end + end + + def params_key + build_key(self.class.params_key) + end + + def password_field + self.class.password_field + end + + # Tries to validate the session from information in the cookie + def persist_by_cookie + creds = cookie_credentials + if creds&.persistence_token.present? + record = search_for_record("find_by_#{klass.primary_key}", creds.record_id) + if record && record.persistence_token == creds.persistence_token + self.unauthorized_record = record + end + valid? + else + false + end + end + + def persist_by_params + return false unless params_enabled? + self.unauthorized_record = search_for_record( + "find_by_single_access_token", + params_credentials + ) + self.single_access = valid? + end + + def persist_by_http_auth + login_proc = http_auth_login_proc + + if self.class.request_http_basic_auth + controller.authenticate_or_request_with_http_basic( + self.class.http_basic_auth_realm, + &login_proc + ) + else + controller.authenticate_with_http_basic(&login_proc) + end + + false + end + + def persist_by_http_auth? + allow_http_basic_auth? && login_field && password_field + end + + # Tries to validate the session from information in the session + def persist_by_session + persistence_token, record_id = session_credentials + if !persistence_token.nil? + record = persist_by_session_search(persistence_token, record_id) + if record && record.persistence_token == persistence_token + self.unauthorized_record = record + end + valid? + else + false + end + end + + # Allow finding by persistence token, because when records are created + # the session is maintained in a before_save, when there is no id. + # This is done for performance reasons and to save on queries. + def persist_by_session_search(persistence_token, record_id) + if record_id.nil? + search_for_record("find_by_persistence_token", persistence_token.to_s) + else + search_for_record("find_by_#{klass.primary_key}", record_id.to_s) + end + end + + def reset_stale_state + self.stale_record = nil + end + + def reset_perishable_token! + if record.respond_to?(:reset_perishable_token) && + !record.disable_perishable_token_maintenance? + record.reset_perishable_token + end + end + + # @api private + def required_magic_states_for(record) + %i[active approved confirmed].select { |state| + record.respond_to?("#{state}?") + } + end + + def reset_failed_login_count? + exceeded_failed_logins_limit? && !being_brute_force_protected? + end + + def reset_failed_login_count + attempted_record.failed_login_count = 0 + end + + # @api private + def run_the_after_validation_callbacks + run_callbacks(new_session? ? :after_validation_on_create : :after_validation_on_update) + run_callbacks(:after_validation) + end + + # @api private + def run_the_before_validation_callbacks + run_callbacks(:before_validation) + run_callbacks(new_session? ? :before_validation_on_create : :before_validation_on_update) + end + + # `args[0]` is the name of a model method, like + # `find_by_single_access_token` or `find_by_smart_case_login_field`. + def search_for_record(*args) + search_scope.scoping do + klass.send(*args) + end + end + + # Returns an AR relation representing the scope of the search. The + # relation is either provided directly by, or defined by + # `find_options`. + def search_scope + if scope[:find_options].is_a?(ActiveRecord::Relation) + scope[:find_options] + else + conditions = scope[:find_options] && scope[:find_options][:conditions] || {} + klass.send(:where, conditions) + end + end + + # @api private + def set_last_request_at + current_time = Time.current + MagicColumn::AssignsLastRequestAt + .new(current_time, record, controller, last_request_at_threshold) + .assign + end + + def single_access? + single_access == true + end + + def single_access_allowed_request_types + self.class.single_access_allowed_request_types + end + + def save_cookie + cookie_jar[cookie_key] = generate_cookie_for_saving + end + + # @api private + # @return [String] - Examples: + # - user_credentials_id + # - ziggity_zack_user_credentials_id + # - ziggity_zack is an "id", see `#id` + # - see persistence_token_test.rb + def session_compound_key + "#{session_key}_#{klass.primary_key}" + end + + def session_credentials + [ + controller.session[session_key], + controller.session[session_compound_key] + ].collect { |i| i.nil? ? i : i.to_s }.compact + end + + # @return [String] - Examples: + # - user_credentials + # - ziggity_zack_user_credentials + # - ziggity_zack is an "id", see `#id` + # - see persistence_token_test.rb + def session_key + build_key(self.class.session_key) + end + + def update_info + increment_login_count + clear_failed_login_count + update_login_timestamps + update_login_ip_addresses + end + + def update_login_ip_addresses + if record.respond_to?(:current_login_ip) + record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip) + record.current_login_ip = controller.request.ip + end + end + + def update_login_timestamps + if record.respond_to?(:current_login_at) + record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at) + record.current_login_at = Time.current + end + end + + def update_session + update_session_set_persistence_token + update_session_set_primary_key + end + + # Updates the session, setting the primary key (usually `id`) of the + # record. + # + # @api private + def update_session_set_primary_key + compound_key = session_compound_key + controller.session[compound_key] = record && record.send(record.class.primary_key) + end + + # Updates the session, setting the `persistence_token` of the record. + # + # @api private + def update_session_set_persistence_token + controller.session[session_key] = record && record.persistence_token + end + + # In keeping with the metaphor of ActiveRecord, verification of the + # password is referred to as a "validation". + def validate_by_password + self.invalid_password = false + validate_by_password__blank_fields + return if errors.count > 0 + self.attempted_record = search_for_record( + self.class.record_selection_method, + send(login_field) + ) + if attempted_record.blank? + add_login_not_found_error + return + end + validate_by_password__invalid_password + end + + def validate_by_password__blank_fields + if send(login_field).blank? + errors.add( + login_field, + I18n.t("error_messages.login_blank", default: "cannot be blank") + ) + end + if send("protected_#{password_field}").blank? + errors.add( + password_field, + I18n.t("error_messages.password_blank", default: "cannot be blank") + ) + end + end + + # Verify the password, usually using `valid_password?` in + # `acts_as_authentic/password.rb`. If it cannot be verified, we + # refer to it as "invalid". + def validate_by_password__invalid_password + unless attempted_record.send( + verify_password_method, + send("protected_#{password_field}") + ) + self.invalid_password = true + add_invalid_password_error + end + end + + def validate_by_unauthorized_record + self.attempted_record = unauthorized_record + end + + def validate_magic_states + return true if attempted_record.nil? + required_magic_states_for(attempted_record).each do |required_status| + next if attempted_record.send("#{required_status}?") + errors.add( + :base, + I18n.t( + "error_messages.not_#{required_status}", + default: "Your account is not #{required_status}" + ) + ) + return false + end + true + end + + def validate_failed_logins + # Clear all other error messages, as they are irrelevant at this point and can + # only provide additional information that is not needed + errors.clear + duration = failed_login_ban_for == 0 ? "" : " temporarily" + errors.add( + :base, + I18n.t( + "error_messages.consecutive_failed_logins_limit_exceeded", + default: format( + "Consecutive failed logins limit exceeded, account has been%s disabled.", + duration + ) + ) + ) + end + + def verify_password_method + self.class.verify_password_method + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/session/brute_force_protection.rb b/lib/authlogic/session/brute_force_protection.rb deleted file mode 100644 index 769eceb7..00000000 --- a/lib/authlogic/session/brute_force_protection.rb +++ /dev/null @@ -1,96 +0,0 @@ -module Authlogic - module Session - # A brute force attacks is executed by hammering a login with as many password combinations as possible, until one works. A brute force attacked is - # generally combated with a slow hasing algorithm such as BCrypt. You can increase the cost, which makes the hash generation slower, and ultimately - # increases the time it takes to execute a brute force attack. Just to put this into perspective, if a hacker was to gain access to your server - # and execute a brute force attack locally, meaning there is no network lag, it would probably take decades to complete. Now throw in network lag - # and it would take MUCH longer. - # - # But for those that are extra paranoid and can't get enough protection, why not stop them as soon as you realize something isn't right? That's - # what this module is all about. By default the consecutive_failed_logins_limit configuration option is set to 50, if someone consecutively fails to login - # after 50 attempts their account will be suspended. This is a very liberal number and at this point it should be obvious that something is not right. - # If you wish to lower this number just set the configuration to a lower number: - # - # class UserSession < Authlogic::Session::Base - # consecutive_failed_logins_limit 10 - # end - module BruteForceProtection - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - validate :reset_failed_login_count, :if => :reset_failed_login_count? - validate :validate_failed_logins, :if => :being_brute_force_protected? - end - end - - # Configuration for the brute force protection feature. - module Config - # To help protect from brute force attacks you can set a limit on the allowed number of consecutive failed logins. By default this is 50, this is a very liberal - # number, and if someone fails to login after 50 tries it should be pretty obvious that it's a machine trying to login in and very likely a brute force attack. - # - # In order to enable this field your model MUST have a failed_login_count (integer) field. - # - # If you don't know what a brute force attack is, it's when a machine tries to login into a system using every combination of character possible. Thus resulting - # in possibly millions of attempts to log into an account. - # - # * Default: 50 - # * Accepts: Integer, set to 0 to disable - def consecutive_failed_logins_limit(value = nil) - rw_config(:consecutive_failed_logins_limit, value, 50) - end - alias_method :consecutive_failed_logins_limit=, :consecutive_failed_logins_limit - - # Once the failed logins limit has been exceed, how long do you want to ban the user? This can be a temporary or permanent ban. - # - # * Default: 2.hours - # * Accepts: Fixnum, set to 0 for permanent ban - def failed_login_ban_for(value = nil) - rw_config(:failed_login_ban_for, (!value.nil? && value) || value, 2.hours.to_i) - end - alias_method :failed_login_ban_for=, :failed_login_ban_for - end - - # The methods available for an Authlogic::Session::Base object that make up the brute force protection feature. - module InstanceMethods - # Returns true when the consecutive_failed_logins_limit has been exceeded and is being temporarily banned. - # Notice the word temporary, the user will not be permanently banned unless you choose to do so with configuration. - # By default they will be banned for 2 hours. During that 2 hour period this method will return true. - def being_brute_force_protected? - exceeded_failed_logins_limit? && (failed_login_ban_for <= 0 || - (attempted_record.respond_to?(:updated_at) && attempted_record.updated_at >= failed_login_ban_for.seconds.ago)) - end - - private - def exceeded_failed_logins_limit? - !attempted_record.nil? && attempted_record.respond_to?(:failed_login_count) && consecutive_failed_logins_limit > 0 && - attempted_record.failed_login_count && attempted_record.failed_login_count >= consecutive_failed_logins_limit - end - - def reset_failed_login_count? - exceeded_failed_logins_limit? && !being_brute_force_protected? - end - - def reset_failed_login_count - attempted_record.failed_login_count = 0 - end - - def validate_failed_logins - errors.clear # Clear all other error messages, as they are irrelevant at this point and can only provide additional information that is not needed - errors.add(:base, I18n.t( - 'error_messages.consecutive_failed_logins_limit_exceeded', - :default => "Consecutive failed logins limit exceeded, account has been" + (failed_login_ban_for == 0 ? "" : " temporarily") + " disabled." - )) - end - - def consecutive_failed_logins_limit - self.class.consecutive_failed_logins_limit - end - - def failed_login_ban_for - self.class.failed_login_ban_for - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/callbacks.rb b/lib/authlogic/session/callbacks.rb deleted file mode 100644 index cce843fe..00000000 --- a/lib/authlogic/session/callbacks.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Authlogic - module Session - # Between these callsbacks and the configuration, this is the contract between me and you to safely - # modify Authlogic's behavior. I will do everything I can to make sure these do not change. - # - # Check out the sub modules of Authlogic::Session. They are very concise, clear, and to the point. More - # importantly they use the same API that you would use to extend Authlogic. That being said, they are great - # examples of how to extend Authlogic and add / modify behavior to Authlogic. These modules could easily be pulled out - # into their own plugin and become an "add on" without any change. - # - # Now to the point of this module. Just like in ActiveRecord you have before_save, before_validation, etc. - # You have similar callbacks with Authlogic, see the METHODS constant below. The order of execution is as follows: - # - # before_persisting - # persist - # after_persisting - # [save record if record.changed?] - # - # before_validation - # before_validation_on_create - # before_validation_on_update - # validate - # after_validation_on_update - # after_validation_on_create - # after_validation - # [save record if record.changed?] - # - # before_save - # before_create - # before_update - # after_update - # after_create - # after_save - # [save record if record.changed?] - # - # before_destroy - # [save record if record.changed?] - # destroy - # after_destroy - # - # Notice the "save record if changed?" lines above. This helps with performance. If you need to make - # changes to the associated record, there is no need to save the record, Authlogic will do it for you. - # This allows multiple modules to modify the record and execute as few queries as possible. - # - # **WARNING**: unlike ActiveRecord, these callbacks must be set up on the class level: - # - # class UserSession < Authlogic::Session::Base - # before_validation :my_method - # validate :another_method - # # ..etc - # end - # - # You can NOT define a "before_validation" method, this is bad practice and does not allow Authlogic - # to extend properly with multiple extensions. Please ONLY use the method above. - module Callbacks - METHODS = [ - "before_persisting", "persist", "after_persisting", - "before_validation", "before_validation_on_create", "before_validation_on_update", "validate", - "after_validation_on_update", "after_validation_on_create", "after_validation", - "before_save", "before_create", "before_update", "after_update", "after_create", "after_save", - "before_destroy", "after_destroy" - ] - - def self.included(base) #:nodoc: - base.send :include, ActiveSupport::Callbacks - base.define_callbacks *METHODS - - # If Rails 3, support the new callback syntax - if base.send(base.respond_to?(:singleton_class) ? :singleton_class : :metaclass).method_defined?(:set_callback) - METHODS.each do |method| - base.class_eval <<-"end_eval", __FILE__, __LINE__ - def self.#{method}(*methods, &block) - set_callback :#{method}, *methods, &block - end - end_eval - end - end - end - - private - METHODS.each do |method| - class_eval <<-"end_eval", __FILE__, __LINE__ - def #{method} - run_callbacks(:#{method}) { |result, object| result == false } - end - end_eval - end - - def persist - run_callbacks(:persist) { |result, object| result == true } - end - - def save_record(alternate_record = nil) - r = alternate_record || record - r.save_without_session_maintenance(:validate => false) if r && r.changed? && !r.readonly? - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/cookies.rb b/lib/authlogic/session/cookies.rb deleted file mode 100644 index 966aaf81..00000000 --- a/lib/authlogic/session/cookies.rb +++ /dev/null @@ -1,130 +0,0 @@ -module Authlogic - module Session - # Handles all authentication that deals with cookies, such as persisting, saving, and destroying. - module Cookies - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - persist :persist_by_cookie - after_save :save_cookie - after_destroy :destroy_cookie - end - end - - # Configuration for the cookie feature set. - module Config - # The name of the cookie or the key in the cookies hash. Be sure and use a unique name. If you have multiple sessions and they use the same cookie it will cause problems. - # Also, if a id is set it will be inserted into the beginning of the string. Exmaple: - # - # session = UserSession.new - # session.cookie_key => "user_credentials" - # - # session = UserSession.new(:super_high_secret) - # session.cookie_key => "super_high_secret_user_credentials" - # - # * Default: "#{guessed_klass_name.underscore}_credentials" - # * Accepts: String - def cookie_key(value = nil) - rw_config(:cookie_key, value, "#{guessed_klass_name.underscore}_credentials") - end - alias_method :cookie_key=, :cookie_key - - # If sessions should be remembered by default or not. - # - # * Default: false - # * Accepts: Boolean - def remember_me(value = nil) - rw_config(:remember_me, value, false) - end - alias_method :remember_me=, :remember_me - - # The length of time until the cookie expires. - # - # * Default: 3.months - # * Accepts: Integer, length of time in seconds, such as 60 or 3.months - def remember_me_for(value = :_read) - rw_config(:remember_me_for, value, 3.months, :_read) - end - alias_method :remember_me_for=, :remember_me_for - end - - # The methods available for an Authlogic::Session::Base object that make up the cookie feature set. - module InstanceMethods - # Allows you to set the remember_me option when passing credentials. - def credentials=(value) - super - values = value.is_a?(Array) ? value : [value] - case values.first - when Hash - self.remember_me = values.first.with_indifferent_access[:remember_me] if values.first.with_indifferent_access.key?(:remember_me) - else - r = values.find { |value| value.is_a?(TrueClass) || value.is_a?(FalseClass) } - self.remember_me = r if !r.nil? - end - end - - # Is the cookie going to expire after the session is over, or will it stick around? - def remember_me - return @remember_me if defined?(@remember_me) - @remember_me = self.class.remember_me - end - - # Accepts a boolean as a flag to remember the session or not. Basically to expire the cookie at the end of the session or keep it for "remember_me_until". - def remember_me=(value) - @remember_me = value - end - - # See remember_me - def remember_me? - remember_me == true || remember_me == "true" || remember_me == "1" - end - - # How long to remember the user if remember_me is true. This is based on the class level configuration: remember_me_for - def remember_me_for - return unless remember_me? - self.class.remember_me_for - end - - # When to expire the cookie. See remember_me_for configuration option to change this. - def remember_me_until - return unless remember_me? - remember_me_for.from_now - end - - private - def cookie_key - build_key(self.class.cookie_key) - end - - def cookie_credentials - controller.cookies[cookie_key] && controller.cookies[cookie_key].split("::") - end - - # Tries to validate the session from information in the cookie - def persist_by_cookie - persistence_token, record_id = cookie_credentials - if !persistence_token.nil? - record = record_id.nil? ? search_for_record("find_by_persistence_token", persistence_token) : search_for_record("find_by_#{klass.primary_key}", record_id) - self.unauthorized_record = record if record && record.persistence_token == persistence_token - valid? - else - false - end - end - - def save_cookie - controller.cookies[cookie_key] = { - :value => "#{record.persistence_token}::#{record.send(record.class.primary_key)}", - :expires => remember_me_until, - :domain => controller.cookie_domain - } - end - - def destroy_cookie - controller.cookies.delete cookie_key, :domain => controller.cookie_domain - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/existence.rb b/lib/authlogic/session/existence.rb deleted file mode 100644 index e2792bbf..00000000 --- a/lib/authlogic/session/existence.rb +++ /dev/null @@ -1,93 +0,0 @@ -module Authlogic - module Session - # Provides methods to create and destroy objects. Basically controls their "existence". - module Existence - class SessionInvalidError < ::StandardError # :nodoc: - def initialize(session) - super("Your session is invalid and has the following errors: #{session.errors.full_messages.to_sentence}") - end - end - - def self.included(klass) - klass.class_eval do - extend ClassMethods - include InstanceMethods - attr_accessor :new_session, :record - end - end - - module ClassMethods - # A convenince method. The same as: - # - # session = UserSession.new(*args) - # session.save - # - # Instead you can do: - # - # UserSession.create(*args) - def create(*args, &block) - session = new(*args) - session.save(&block) - session - end - - # Same as create but calls create!, which raises an exception when validation fails. - def create!(*args) - session = new(*args) - session.save! - session - end - end - - module InstanceMethods - # Clears all errors and the associated record, you should call this terminate a session, thus requring - # the user to authenticate again if it is needed. - def destroy - before_destroy - save_record - errors.clear - @record = nil - after_destroy - true - end - - # Returns true if the session is new, meaning no action has been taken on it and a successful save - # has not taken place. - def new_session? - new_session != false - end - - # After you have specified all of the details for your session you can try to save it. This will - # run validation checks and find the associated record, if all validation passes. If validation - # does not pass, the save will fail and the erorrs will be stored in the errors object. - def save(&block) - result = nil - if valid? - self.record = attempted_record - - before_save - new_session? ? before_create : before_update - new_session? ? after_create : after_update - after_save - - save_record - self.new_session = false - result = true - else - result = false - end - - yield result if block_given? - result - end - - # Same as save but raises an exception of validation errors when validation fails - def save! - result = save - raise SessionInvalidError.new(self) unless result - result - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/foundation.rb b/lib/authlogic/session/foundation.rb deleted file mode 100644 index f2fc1769..00000000 --- a/lib/authlogic/session/foundation.rb +++ /dev/null @@ -1,71 +0,0 @@ -module Authlogic - module Session - # Sort of like an interface, it sets the foundation for the class, such as the required methods. This also allows - # other modules to overwrite methods and call super on them. It's also a place to put "utility" methods used - # throughout Authlogic. - module Foundation - def self.included(klass) - klass.class_eval do - extend ClassMethods - include InstanceMethods - end - end - - module ClassMethods - private - def rw_config(key, value, default_value = nil, read_value = nil) - if value == read_value - return read_inheritable_attribute(key) if inheritable_attributes.include?(key) - write_inheritable_attribute(key, default_value) - else - write_inheritable_attribute(key, value) - end - end - end - - module InstanceMethods - def initialize(*args) - self.credentials = args - end - - # The credentials you passed to create your session. See credentials= for more info. - def credentials - [] - end - - # Set your credentials before you save your session. You can pass a hash of credentials: - # - # session.credentials = {:login => "my login", :password => "my password", :remember_me => true} - # - # or you can pass an array of objects: - # - # session.credentials = [my_user_object, true] - # - # and if you need to set an id, just pass it last. This value need be the last item in the array you pass, since the id is something that - # you control yourself, it should never be set from a hash or a form. Examples: - # - # session.credentials = [{:login => "my login", :password => "my password", :remember_me => true}, :my_id] - # session.credentials = [my_user_object, true, :my_id] - def credentials=(values) - end - - def inspect - "#<#{self.class.name}: #{credentials.blank? ? "no credentials provided" : credentials.inspect}>" - end - - def persisted? - !(new_record? || destroyed?) - end - - def to_key - new_record? ? nil : [ self.send(self.class.primary_key) ] - end - - private - def build_key(last_part) - last_part - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/http_auth.rb b/lib/authlogic/session/http_auth.rb deleted file mode 100644 index 73a1a2f5..00000000 --- a/lib/authlogic/session/http_auth.rb +++ /dev/null @@ -1,58 +0,0 @@ -module Authlogic - module Session - # Handles all authentication that deals with basic HTTP auth. Which is authentication built into the HTTP protocol: - # - # http://username:password@whatever.com - # - # Also, if you are not comfortable letting users pass their raw username and password you can always use the single - # access token. See Authlogic::Session::Params for more info. - module HttpAuth - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - persist :persist_by_http_auth, :if => :persist_by_http_auth? - end - end - - # Configuration for the HTTP basic auth feature of Authlogic. - module Config - # Do you want to allow your users to log in via HTTP basic auth? - # - # I recommend keeping this enabled. The only time I feel this should be disabled is if you are not comfortable - # having your users provide their raw username and password. Whatever the reason, you can disable it here. - # - # * Default: true - # * Accepts: Boolean - def allow_http_basic_auth(value = nil) - rw_config(:allow_http_basic_auth, value, true) - end - alias_method :allow_http_basic_auth=, :allow_http_basic_auth - end - - # Instance methods for the HTTP basic auth feature of authlogic. - module InstanceMethods - private - def persist_by_http_auth? - allow_http_basic_auth? && login_field && password_field - end - - def persist_by_http_auth - controller.authenticate_with_http_basic do |login, password| - if !login.blank? && !password.blank? - send("#{login_field}=", login) - send("#{password_field}=", password) - return valid? - end - end - - false - end - - def allow_http_basic_auth? - self.class.allow_http_basic_auth == true - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/id.rb b/lib/authlogic/session/id.rb deleted file mode 100644 index b127bc97..00000000 --- a/lib/authlogic/session/id.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Authlogic - module Session - # Allows you to separate sessions with an id, ultimately letting you create multiple sessions for the same user. - module Id - def self.included(klass) - klass.class_eval do - attr_writer :id - end - end - - # Setting the id if it is passed in the credentials. - def credentials=(value) - super - values = value.is_a?(Array) ? value : [value] - self.id = values.last if values.last.is_a?(Symbol) - end - - # Allows you to set a unique identifier for your session, so that you can have more than 1 session at a time. - # A good example when this might be needed is when you want to have a normal user session and a "secure" user session. - # The secure user session would be created only when they want to modify their billing information, or other sensitive - # information. Similar to me.com. This requires 2 user sessions. Just use an id for the "secure" session and you should be good. - # - # You can set the id during initialization (see initialize for more information), or as an attribute: - # - # session.id = :my_id - # - # Just be sure and set your id before you save your session. - # - # Lastly, to retrieve your session with the id check out the find class method. - def id - @id - end - - private - # Used for things like cookie_key, session_key, etc. - def build_key(last_part) - [id, super].compact.join("_") - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/klass.rb b/lib/authlogic/session/klass.rb deleted file mode 100644 index a94ab8fb..00000000 --- a/lib/authlogic/session/klass.rb +++ /dev/null @@ -1,78 +0,0 @@ -module Authlogic - module Session - # Handles authenticating via a traditional username and password. - module Klass - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - - class << self - attr_accessor :configured_klass_methods - end - end - end - - module Config - # Lets you change which model to use for authentication. - # - # * Default: inferred from the class name. UserSession would automatically try User - # * Accepts: an ActiveRecord class - def authenticate_with(klass) - @klass_name = klass.name - @klass = klass - end - alias_method :authenticate_with=, :authenticate_with - - # The name of the class that this session is authenticating with. For example, the UserSession class will - # authenticate with the User class unless you specify otherwise in your configuration. See authenticate_with - # for information on how to change this value. - def klass - @klass ||= - if klass_name - klass_name.constantize - else - nil - end - end - - # Same as klass, just returns a string instead of the actual constant. - def klass_name - @klass_name ||= guessed_klass_name - end - - # The string of the model name class guessed from the actual session class name. - def guessed_klass_name - guessed_name = name.scan(/(.*)Session/)[0] - guessed_name[0] if guessed_name - end - end - - module InstanceMethods - # Creating an alias method for the "record" method based on the klass name, so that we can do: - # - # session.user - # - # instead of: - # - # session.record - def initialize(*args) - if !self.class.configured_klass_methods - self.class.send(:alias_method, klass_name.demodulize.underscore.to_sym, :record) - self.class.configured_klass_methods = true - end - super - end - - private - def klass - self.class.klass - end - - def klass_name - self.class.klass_name - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/magic_column/assigns_last_request_at.rb b/lib/authlogic/session/magic_column/assigns_last_request_at.rb new file mode 100644 index 00000000..465b9855 --- /dev/null +++ b/lib/authlogic/session/magic_column/assigns_last_request_at.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Authlogic + module Session + module MagicColumn + # Assigns the current time to the `last_request_at` attribute. + # + # 1. The `last_request_at` column must exist + # 2. Assignment can be disabled on a per-controller basis + # 3. Assignment will not happen more often than `last_request_at_threshold` + # seconds. + # + # - current_time - a `Time` + # - record - eg. a `User` + # - controller - an `Authlogic::ControllerAdapters::AbstractAdapter` + # - last_request_at_threshold - integer - seconds + # + # @api private + class AssignsLastRequestAt + def initialize(current_time, record, controller, last_request_at_threshold) + @current_time = current_time + @record = record + @controller = controller + @last_request_at_threshold = last_request_at_threshold + end + + def assign + return unless assign? + @record.last_request_at = @current_time + end + + private + + # @api private + def assign? + @record && + @record.class.column_names.include?("last_request_at") && + @controller.last_request_update_allowed? && ( + @record.last_request_at.blank? || + @last_request_at_threshold.to_i.seconds.ago >= @record.last_request_at + ) + end + end + end + end +end diff --git a/lib/authlogic/session/magic_columns.rb b/lib/authlogic/session/magic_columns.rb deleted file mode 100644 index 127dbb5a..00000000 --- a/lib/authlogic/session/magic_columns.rb +++ /dev/null @@ -1,95 +0,0 @@ -module Authlogic - module Session - # Just like ActiveRecord has "magic" columns, such as: created_at and updated_at. Authlogic has its own "magic" columns too: - # - # Column name Description - # login_count Increased every time an explicit login is made. This will *NOT* increase if logging in by a session, cookie, or basic http auth - # failed_login_count This increases for each consecutive failed login. See Authlogic::Session::BruteForceProtection and the consecutive_failed_logins_limit config option for more details. - # last_request_at Updates every time the user logs in, either by explicitly logging in, or logging in by cookie, session, or http auth - # current_login_at Updates with the current time when an explicit login is made. - # last_login_at Updates with the value of current_login_at before it is reset. - # current_login_ip Updates with the request remote_ip when an explicit login is made. - # last_login_ip Updates with the value of current_login_ip before it is reset. - module MagicColumns - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - after_persisting :set_last_request_at, :if => :set_last_request_at? - validate :increase_failed_login_count - before_save :update_info - before_save :set_last_request_at, :if => :set_last_request_at? - end - end - - # Configuration for the magic columns feature. - module Config - # Every time a session is found the last_request_at field for that record is updatd with the current time, if that field exists. - # If you want to limit how frequent that field is updated specify the threshold here. For example, if your user is making a - # request every 5 seconds, and you feel this is too frequent, and feel a minute is a good threashold. Set this to 1.minute. - # Once a minute has passed in between requests the field will be updated. - # - # * Default: 0 - # * Accepts: integer representing time in seconds - def last_request_at_threshold(value = nil) - rw_config(:last_request_at_threshold, value, 0) - end - alias_method :last_request_at_threshold=, :last_request_at_threshold - end - - # The methods available for an Authlogic::Session::Base object that make up the magic columns feature. - module InstanceMethods - private - def increase_failed_login_count - if invalid_password? && attempted_record.respond_to?(:failed_login_count) - attempted_record.failed_login_count ||= 0 - attempted_record.failed_login_count += 1 - end - end - - def update_info - record.login_count = (record.login_count.blank? ? 1 : record.login_count + 1) if record.respond_to?(:login_count) - record.failed_login_count = 0 if record.respond_to?(:failed_login_count) - - if record.respond_to?(:current_login_at) - record.last_login_at = record.current_login_at if record.respond_to?(:last_login_at) - record.current_login_at = klass.default_timezone == :utc ? Time.now.utc : Time.now - end - - if record.respond_to?(:current_login_ip) - record.last_login_ip = record.current_login_ip if record.respond_to?(:last_login_ip) - record.current_login_ip = controller.request.remote_ip - end - end - - # This method lets authlogic know whether it should allow the last_request_at field to be updated - # with the current time (Time.now). One thing to note here is that it also checks for the existence of a - # last_request_update_allowed? method in your controller. This allows you to control this method pragmatically - # in your controller. - # - # For example, what if you had a javascript function that polled the server updating how much time is left in their - # session before it times out. Obviously you would want to ignore this request, because then the user would never time out. - # So you can do something like this in your controller: - # - # def last_request_update_allowed? - # action_name =! "update_session_time_left" - # end - # - # You can do whatever you want with that method. - def set_last_request_at? # :doc: - return false if !record || !klass.column_names.include?("last_request_at") - return controller.last_request_update_allowed? if controller.responds_to_last_request_update_allowed? - record.last_request_at.blank? || last_request_at_threshold.to_i.seconds.ago >= record.last_request_at - end - - def set_last_request_at - record.last_request_at = klass.default_timezone == :utc ? Time.now.utc : Time.now - end - - def last_request_at_threshold - self.class.last_request_at_threshold - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/magic_states.rb b/lib/authlogic/session/magic_states.rb deleted file mode 100644 index f896eb27..00000000 --- a/lib/authlogic/session/magic_states.rb +++ /dev/null @@ -1,59 +0,0 @@ -module Authlogic - module Session - # Authlogic tries to check the state of the record before creating the session. If your record responds to the following methods and any of them return false, validation will fail: - # - # Method name Description - # active? Is the record marked as active? - # approved? Has the record been approved? - # confirmed? Has the record been conirmed? - # - # Authlogic does nothing to define these methods for you, its up to you to define what they mean. If your object responds to these methods Authlogic will use them, otherwise they are ignored. - # - # What's neat about this is that these are checked upon any type of login. When logging in explicitly, by cookie, session, or basic http auth. - # So if you mark a user inactive in the middle of their session they wont be logged back in next time they refresh the page. Giving you complete control. - # - # Need Authlogic to check your own "state"? No problem, check out the hooks section below. Add in a before_validation to do your own checking. The sky is the limit. - module MagicStates - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - validate :validate_magic_states, :unless => :disable_magic_states? - end - end - - # Configuration for the magic states feature. - module Config - # Set this to true if you want to disable the checking of active?, approved?, and confirmed? on your record. This is more or less of a - # convenience feature, since 99% of the time if those methods exist and return false you will not want the user logging in. You could - # easily accomplish this same thing with a before_validation method or other callbacks. - # - # * Default: false - # * Accepts: Boolean - def disable_magic_states(value = nil) - rw_config(:disable_magic_states, value, false) - end - alias_method :disable_magic_states=, :disable_magic_states - end - - # The methods available for an Authlogic::Session::Base object that make up the magic states feature. - module InstanceMethods - private - def disable_magic_states? - self.class.disable_magic_states == true - end - - def validate_magic_states - return true if attempted_record.nil? - [:active, :approved, :confirmed].each do |required_status| - if attempted_record.respond_to?("#{required_status}?") && !attempted_record.send("#{required_status}?") - errors.add(:base, I18n.t("error_messages.not_#{required_status}", :default => "Your account is not #{required_status}")) - return false - end - end - true - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/params.rb b/lib/authlogic/session/params.rb deleted file mode 100644 index 39707a03..00000000 --- a/lib/authlogic/session/params.rb +++ /dev/null @@ -1,101 +0,0 @@ -module Authlogic - module Session - # This module is responsible for authenticating the user via params, which ultimately allows the user to log in using a URL like the following: - # - # https://www.domain.com?user_credentials=4LiXF7FiGUppIPubBPey - # - # Notice the token in the URL, this is a single access token. A single access token is used for single access only, it is not persisted. Meaning the user - # provides it, Authlogic grants them access, and that's it. If they want access again they need to provide the token again. Authlogic will - # *NEVER* try to persist the session after authenticating through this method. - # - # For added security, this token is *ONLY* allowed for RSS and ATOM requests. You can change this with the configuration. You can also define if - # it is allowed dynamically by defining a single_access_allowed? method in your controller. For example: - # - # class UsersController < ApplicationController - # private - # def single_access_allowed? - # action_name == "index" - # end - # - # Also, by default, this token is permanent. Meaning if the user changes their password, this token will remain the same. It will only change - # when it is explicitly reset. - # - # You can modify all of this behavior with the Config sub module. - module Params - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - attr_accessor :single_access - persist :persist_by_params - end - end - - # Configuration for the params / single access feature. - module Config - # Works exactly like cookie_key, but for params. So a user can login via params just like a cookie or a session. Your URL would look like: - # - # http://www.domain.com?user_credentials=my_single_access_key - # - # You can change the "user_credentials" key above with this configuration option. Keep in mind, just like cookie_key, if you supply an id - # the id will be appended to the front. Check out cookie_key for more details. Also checkout the "Single Access / Private Feeds Access" section in the README. - # - # * Default: cookie_key - # * Accepts: String - def params_key(value = nil) - rw_config(:params_key, value, cookie_key) - end - alias_method :params_key=, :params_key - - # Authentication is allowed via a single access token, but maybe this is something you don't want for your application as a whole. Maybe this is - # something you only want for specific request types. Specify a list of allowed request types and single access authentication will only be - # allowed for the ones you specify. - # - # * Default: ["application/rss+xml", "application/atom+xml"] - # * Accepts: String of a request type, or :all or :any to allow single access authentication for any and all request types - def single_access_allowed_request_types(value = nil) - rw_config(:single_access_allowed_request_types, value, ["application/rss+xml", "application/atom+xml"]) - end - alias_method :single_access_allowed_request_types=, :single_access_allowed_request_types - end - - # The methods available for an Authlogic::Session::Base object that make up the params / single access feature. - module InstanceMethods - private - def persist_by_params - return false if !params_enabled? - self.unauthorized_record = search_for_record("find_by_single_access_token", params_credentials) - self.single_access = valid? - end - - def params_enabled? - return false if !params_credentials || !klass.column_names.include?("single_access_token") - return controller.single_access_allowed? if controller.responds_to_single_access_allowed? - - case single_access_allowed_request_types - when Array - single_access_allowed_request_types.include?(controller.request_content_type) || single_access_allowed_request_types.include?(:all) - else - [:all, :any].include?(single_access_allowed_request_types) - end - end - - def params_key - build_key(self.class.params_key) - end - - def single_access? - single_access == true - end - - def single_access_allowed_request_types - self.class.single_access_allowed_request_types - end - - def params_credentials - controller.params[params_key] - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/password.rb b/lib/authlogic/session/password.rb deleted file mode 100644 index 4e769571..00000000 --- a/lib/authlogic/session/password.rb +++ /dev/null @@ -1,240 +0,0 @@ -module Authlogic - module Session - # Handles authenticating via a traditional username and password. - module Password - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - validate :validate_by_password, :if => :authenticating_with_password? - - class << self - attr_accessor :configured_password_methods - end - end - end - - # Password configuration - module Config - # Authlogic tries to validate the credentials passed to it. One part of validation is actually finding the user and - # making sure it exists. What method it uses the do this is up to you. - # - # Let's say you have a UserSession that is authenticating a User. By default UserSession will call User.find_by_login(login). - # You can change what method UserSession calls by specifying it here. Then in your User model you can make that method do - # anything you want, giving you complete control of how users are found by the UserSession. - # - # Let's take an example: You want to allow users to login by username or email. Set this to the name of the class method - # that does this in the User model. Let's call it "find_by_username_or_email" - # - # class User < ActiveRecord::Base - # def self.find_by_username_or_email(login) - # find_by_username(login) || find_by_email(login) - # end - # end - # - # Now just specify the name of this method for this configuration option and you are all set. You can do anything you - # want here. Maybe you allow users to have multiple logins and you want to search a has_many relationship, etc. The sky is the limit. - # - # * Default: "find_by_smart_case_login_field" - # * Accepts: Symbol or String - def find_by_login_method(value = nil) - rw_config(:find_by_login_method, value, "find_by_smart_case_login_field") - end - alias_method :find_by_login_method=, :find_by_login_method - - # The text used to identify credentials (username/password) combination when a bad login attempt occurs. - # When you show error messages for a bad login, it's considered good security practice to hide which field - # the user has entered incorrectly (the login field or the password field). For a full explanation, see - # http://www.gnucitizen.org/blog/username-enumeration-vulnerabilities/ - # - # Example of use: - # - # class UserSession < Authlogic::Session::Base - # generalize_credentials_error_messages true - # end - # - # This would make the error message for bad logins and bad passwords look identical: - # - # Login/Password combination is not valid - # - # Alternatively you may use a custom message: - # - # class UserSession < AuthLogic::Session::Base - # generalize_credentials_error_messages "Your login information is invalid" - # end - # - # This will instead show your custom error message when the UserSession is invalid. - # - # The downside to enabling this is that is can be too vague for a user that has a hard time remembering - # their username and password combinations. It also disables the ability to to highlight the field - # with the error when you use form_for. - # - # If you are developing an app where security is an extreme priority (such as a financial application), - # then you should enable this. Otherwise, leaving this off is fine. - # - # * Default false - # * Accepts: Boolean - def generalize_credentials_error_messages(value = nil) - rw_config(:generalize_credentials_error_messages, value, false) - end - alias_method :generalize_credentials_error_messages=, :generalize_credentials_error_messages - - # The name of the method you want Authlogic to create for storing the login / username. Keep in mind this is just for your - # Authlogic::Session, if you want it can be something completely different than the field in your model. So if you wanted people to - # login with a field called "login" and then find users by email this is compeltely doable. See the find_by_login_method configuration - # option for more details. - # - # * Default: klass.login_field || klass.email_field - # * Accepts: Symbol or String - def login_field(value = nil) - rw_config(:login_field, value, klass.login_field || klass.email_field) - end - alias_method :login_field=, :login_field - - # Works exactly like login_field, but for the password instead. Returns :password if a login_field exists. - # - # * Default: :password - # * Accepts: Symbol or String - def password_field(value = nil) - rw_config(:password_field, value, login_field && :password) - end - alias_method :password_field=, :password_field - - # The name of the method in your model used to verify the password. This should be an instance method. It should also - # be prepared to accept a raw password and a crytped password. - # - # * Default: "valid_password?" - # * Accepts: Symbol or String - def verify_password_method(value = nil) - rw_config(:verify_password_method, value, "valid_password?") - end - alias_method :verify_password_method=, :verify_password_method - end - - # Password related instance methods - module InstanceMethods - def initialize(*args) - if !self.class.configured_password_methods - if login_field - self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=") - self.class.send(:attr_reader, login_field) if !respond_to?(login_field) - end - - if password_field - self.class.send(:attr_writer, password_field) if !respond_to?("#{password_field}=") - self.class.send(:define_method, password_field) {} if !respond_to?(password_field) - - self.class.class_eval <<-"end_eval", __FILE__, __LINE__ - private - # The password should not be accessible publicly. This way forms using form_for don't fill the password with the - # attempted password. To prevent this we just create this method that is private. - def protected_#{password_field} - @#{password_field} - end - end_eval - end - - self.class.configured_password_methods = true - end - - super - end - - # Returns the login_field / password_field credentials combination in hash form. - def credentials - if authenticating_with_password? - details = {} - details[login_field.to_sym] = send(login_field) - details[password_field.to_sym] = "" - details - else - super - end - end - - # Accepts the login_field / password_field credentials combination in hash form. - def credentials=(value) - super - values = value.is_a?(Array) ? value : [value] - if values.first.is_a?(Hash) - values.first.with_indifferent_access.slice(login_field, password_field).each do |field, value| - next if value.blank? - send("#{field}=", value) - end - end - end - - def invalid_password? - invalid_password == true - end - - private - def authenticating_with_password? - login_field && (!send(login_field).nil? || !send("protected_#{password_field}").nil?) - end - - def validate_by_password - self.invalid_password = false - - errors.add(login_field, I18n.t('error_messages.login_blank', :default => "cannot be blank")) if send(login_field).blank? - errors.add(password_field, I18n.t('error_messages.password_blank', :default => "cannot be blank")) if send("protected_#{password_field}").blank? - return if errors.count > 0 - - self.attempted_record = search_for_record(find_by_login_method, send(login_field)) - if attempted_record.blank? - generalize_credentials_error_messages? ? - add_general_credentials_error : - errors.add(login_field, I18n.t('error_messages.login_not_found', :default => "is not valid")) - return - end - - if !attempted_record.send(verify_password_method, send("protected_#{password_field}")) - self.invalid_password = true - generalize_credentials_error_messages? ? - add_general_credentials_error : - errors.add(password_field, I18n.t('error_messages.password_invalid', :default => "is not valid")) - return - end - end - - def invalid_password - @invalid_password - end - - def invalid_password=(value) - @invalid_password = value - end - - def find_by_login_method - self.class.find_by_login_method - end - - def login_field - self.class.login_field - end - - def add_general_credentials_error - error_message = - if self.class.generalize_credentials_error_messages.is_a? String - self.class.generalize_credentials_error_messages - else - "#{login_field.to_s.humanize}/Password combination is not valid" - end - errors.add(:base, I18n.t('error_messages.general_credentials_error', :default => error_message)) - end - - def generalize_credentials_error_messages? - self.class.generalize_credentials_error_messages - end - - def password_field - self.class.password_field - end - - def verify_password_method - self.class.verify_password_method - end - end - end - end -end diff --git a/lib/authlogic/session/perishable_token.rb b/lib/authlogic/session/perishable_token.rb deleted file mode 100644 index 8f9b0798..00000000 --- a/lib/authlogic/session/perishable_token.rb +++ /dev/null @@ -1,18 +0,0 @@ -module Authlogic - module Session - # Maintains the perishable token, which is helpful for confirming records or authorizing records to reset their password. All that this - # module does is reset it after a session have been saved, just keep it changing. The more it changes, the tighter the security. - # - # See Authlogic::ActsAsAuthentic::PerishableToken for more information. - module PerishableToken - def self.included(klass) - klass.after_save :reset_perishable_token! - end - - private - def reset_perishable_token! - record.reset_perishable_token if record.respond_to?(:reset_perishable_token) && !record.disable_perishable_token_maintenance? - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/persistence.rb b/lib/authlogic/session/persistence.rb deleted file mode 100644 index e1eff4a7..00000000 --- a/lib/authlogic/session/persistence.rb +++ /dev/null @@ -1,70 +0,0 @@ -module Authlogic - module Session - # Responsible for allowing you to persist your sessions. - module Persistence - def self.included(klass) - klass.class_eval do - extend ClassMethods - include InstanceMethods - end - end - - module ClassMethods - # This is how you persist a session. This finds the record for the current session using - # a variety of methods. It basically tries to "log in" the user without the user having - # to explicitly log in. Check out the other Authlogic::Session modules for more information. - # - # The best way to use this method is something like: - # - # helper_method :current_user_session, :current_user - # - # def current_user_session - # return @current_user_session if defined?(@current_user_session) - # @current_user_session = UserSession.find - # end - # - # def current_user - # return @current_user if defined?(@current_user) - # @current_user = current_user_session && current_user_session.user - # end - # - # Also, this method accepts a single parameter as the id, to find session that you marked with an id: - # - # UserSession.find(:secure) - # - # See the id method for more information on ids. - def find(id = nil, priority_record = nil) - session = new({:priority_record => priority_record}, id) - session.priority_record = priority_record - if session.persisting? - session - else - nil - end - end - end - - module InstanceMethods - # Let's you know if the session is being persisted or not, meaning the user does not have to explicitly log in - # in order to be logged in. If the session has no associated record, it will try to find a record and persis - # the session. This is the method that the class level method find uses to ultimately persist the session. - def persisting? - return true if !record.nil? - self.attempted_record = nil - before_persisting - persist - ensure_authentication_attempted - if errors.empty? && !attempted_record.nil? - self.record = attempted_record - after_persisting - save_record - self.new_session = false - true - else - false - end - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/priority_record.rb b/lib/authlogic/session/priority_record.rb deleted file mode 100644 index 85a464c0..00000000 --- a/lib/authlogic/session/priority_record.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Authlogic - module Session - # The point of this module is to avoid the StaleObjectError raised when lock_version is implemented in ActiveRecord. - # We accomplish this by using a "priority record". Meaning this record is used if possible, it gets priority. - # This way we don't save a record behind the scenes thus making an object being used stale. - module PriorityRecord - def self.included(klass) - klass.class_eval do - attr_accessor :priority_record - end - end - - # Setting priority record if it is passed. The only way it can be passed is through an array: - # - # session.credentials = [real_user_object, priority_user_object] - def credentials=(value) - super - values = value.is_a?(Array) ? value : [value] - self.priority_record = values[1] if values[1].class < ::ActiveRecord::Base - end - - private - def attempted_record=(value) - value = priority_record if value == priority_record - super - end - - def save_record(alternate_record = nil) - r = alternate_record || record - super if r != priority_record - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/scopes.rb b/lib/authlogic/session/scopes.rb deleted file mode 100644 index 70ca3661..00000000 --- a/lib/authlogic/session/scopes.rb +++ /dev/null @@ -1,101 +0,0 @@ -module Authlogic - module Session - # Authentication can be scoped, and it's easy, you just need to define how you want to scope everything. This should help you: - # - # 1. Want to scope by a parent object? Ex: An account has many users. Checkout Authlogic::AuthenticatesMany - # 2. Want to scope the validations in your model? Ex: 2 users can have the same login under different accounts. See Authlogic::ActsAsAuthentic::Scope - module Scopes # :nodoc: - def self.included(klass) - klass.class_eval do - extend ClassMethods - include InstanceMethods - attr_writer :scope - end - end - - # = Scopes - module ClassMethods - # The current scope set, should be used in the block passed to with_scope. - def scope - Thread.current[:authlogic_scope] - end - - # What with_scopes focuses on is scoping the query when finding the object and the name of the cookie / session. It works very similar to - # ActiveRecord::Base#with_scopes. It accepts a hash with any of the following options: - # - # * find_options: any options you can pass into ActiveRecord::Base.find. This is used when trying to find the record. - # * id: The id of the session, this gets merged with the real id. For information ids see the id method. - # - # Here is how you use it: - # - # UserSession.with_scope(:find_options => {:conditions => "account_id = 2"}, :id => "account_2") do - # UserSession.find - # end - # - # Eseentially what the above does is scope the searching of the object with the sql you provided. So instead of: - # - # User.find(:first, :conditions => "login = 'ben'") - # - # it would be: - # - # User.find(:first, :conditions => "login = 'ben' and account_id = 2") - # - # You will also notice the :id option. This works just like the id method. It scopes your cookies. So the name of your cookie will be: - # - # account_2_user_credentials - # - # instead of: - # - # user_credentials - # - # What is also nifty about scoping with an :id is that it merges your id's. So if you do: - # - # UserSession.with_scope(:find_options => {:conditions => "account_id = 2"}, :id => "account_2") do - # session = UserSession.new - # session.id = :secure - # end - # - # The name of your cookies will be: - # - # secure_account_2_user_credentials - def with_scope(options = {}, &block) - raise ArgumentError.new("You must provide a block") unless block_given? - self.scope = options - result = yield - self.scope = nil - result - end - - private - def scope=(value) - Thread.current[:authlogic_scope] = value - end - end - - module InstanceMethods - # Setting the scope if it exists upon instantiation. - def initialize(*args) - self.scope = self.class.scope - super - end - - # The scope of the current object - def scope - @scope ||= {} - end - - private - # Used for things like cookie_key, session_key, etc. - def build_key(last_part) - [scope[:id], super].compact.join("_") - end - - def search_for_record(*args) - klass.send(:with_scope, :find => (scope[:find_options] || {})) do - klass.send(*args) - end - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/session.rb b/lib/authlogic/session/session.rb deleted file mode 100644 index 565dd760..00000000 --- a/lib/authlogic/session/session.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Authlogic - module Session - # Handles all parts of authentication that deal with sessions. Such as persisting a session and saving / destroy a session. - module Session - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - persist :persist_by_session - after_save :update_session - after_destroy :update_session - after_persisting :update_session, :unless => :single_access? - end - end - - # Configuration for the session feature. - module Config - # Works exactly like cookie_key, but for sessions. See cookie_key for more info. - # - # * Default: cookie_key - # * Accepts: Symbol or String - def session_key(value = nil) - rw_config(:session_key, value, cookie_key) - end - alias_method :session_key=, :session_key - end - - # Instance methods for the session feature. - module InstanceMethods - private - # Tries to validate the session from information in the session - def persist_by_session - persistence_token, record_id = session_credentials - if !persistence_token.nil? - # Allow finding by persistence token, because when records are created the session is maintained in a before_save, when there is no id. - # This is done for performance reasons and to save on queries. - record = record_id.nil? ? - search_for_record("find_by_persistence_token", persistence_token) : - search_for_record("find_by_#{klass.primary_key}", record_id) - self.unauthorized_record = record if record && record.persistence_token == persistence_token - valid? - else - false - end - end - - def session_credentials - [controller.session[session_key], controller.session["#{session_key}_#{klass.primary_key}"]].compact - end - - def session_key - build_key(self.class.session_key) - end - - def update_session - controller.session[session_key] = record && record.persistence_token - controller.session["#{session_key}_#{klass.primary_key}"] = record && record.send(record.class.primary_key) - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/timeout.rb b/lib/authlogic/session/timeout.rb deleted file mode 100644 index 84484dc1..00000000 --- a/lib/authlogic/session/timeout.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Authlogic - module Session - # Think about financial websites, if you are inactive for a certain period of time you will be asked to - # log back in on your next request. You can do this with Authlogic easily, there are 2 parts to this: - # - # 1. Define the timeout threshold: - # - # acts_as_authentic do |c| - # c.logged_in_timeout = 10.minutes # default is 10.minutes - # end - # - # 2. Enable logging out on timeouts - # - # class UserSession < Authlogic::Session::Base - # logout_on_timeout true # default if false - # end - # - # This will require a user to log back in if they are inactive for more than 10 minutes. In order for - # this feature to be used you must have a last_request_at datetime column in your table for whatever model - # you are authenticating with. - module Timeout - def self.included(klass) - klass.class_eval do - extend Config - include InstanceMethods - before_persisting :reset_stale_state - after_persisting :enforce_timeout - attr_accessor :stale_record - end - end - - # Configuration for the timeout feature. - module Config - # With acts_as_authentic you get a :logged_in_timeout configuration option. If this is set, after this amount of time has passed the user - # will be marked as logged out. Obviously, since web based apps are on a per request basis, we have to define a time limit threshold that - # determines when we consider a user to be "logged out". Meaning, if they login and then leave the website, when do mark them as logged out? - # I recommend just using this as a fun feature on your website or reports, giving you a ballpark number of users logged in and active. This is - # not meant to be a dead accurate representation of a users logged in state, since there is really no real way to do this with web based apps. - # Think about a user that logs in and doesn't log out. There is no action that tells you that the user isn't technically still logged in and - # active. - # - # That being said, you can use that feature to require a new login if their session timesout. Similar to how financial sites work. Just set this option to - # true and if your record returns true for stale? then they will be required to log back in. - # - # Lastly, UserSession.find will still return a object is the session is stale, but you will not get a record. This allows you to determine if the - # user needs to log back in because their session went stale, or because they just aren't logged in. Just call current_user_session.stale? as your flag. - # - # * Default: false - # * Accepts: Boolean - def logout_on_timeout(value = nil) - rw_config(:logout_on_timeout, value, false) - end - alias_method :logout_on_timeout=, :logout_on_timeout - end - - # Instance methods for the timeout feature. - module InstanceMethods - # Tells you if the record is stale or not. Meaning the record has timed out. This will only return true if you set logout_on_timeout to true in your configuration. - # Basically how a bank website works. If you aren't active over a certain period of time your session becomes stale and requires you to log back in. - def stale? - !stale_record.nil? || (logout_on_timeout? && record && record.logged_out?) - end - - private - def reset_stale_state - self.stale_record = nil - end - - def enforce_timeout - if stale? - self.stale_record = record - self.record = nil - end - end - - def logout_on_timeout? - self.class.logout_on_timeout == true - end - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/unauthorized_record.rb b/lib/authlogic/session/unauthorized_record.rb deleted file mode 100644 index 8c648bcf..00000000 --- a/lib/authlogic/session/unauthorized_record.rb +++ /dev/null @@ -1,50 +0,0 @@ -module Authlogic - module Session - # Allows you to create session with an object. Ex: - # - # UserSession.create(my_user_object) - # - # Be careful with this, because Authlogic is assuming that you have already confirmed that the - # user is who he says he is. - # - # For example, this is the method used to persist the session internally. Authlogic finds the user with - # the persistence token. At this point we know the user is who he says he is, so Authlogic just creates a - # session with the record. This is particularly useful for 3rd party authentication methods, such as - # OpenID. Let that method verify the identity, once it's verified, pass the object and create a session. - module UnauthorizedRecord - def self.included(klass) - klass.class_eval do - attr_accessor :unauthorized_record - validate :validate_by_unauthorized_record, :if => :authenticating_with_unauthorized_record? - end - end - - # Returning meaningful credentials - def credentials - if authenticating_with_unauthorized_record? - details = {} - details[:unauthorized_record] = "" - details - else - super - end - end - - # Setting the unauthorized record if it exists in the credentials passed. - def credentials=(value) - super - values = value.is_a?(Array) ? value : [value] - self.unauthorized_record = values.first if values.first.class < ::ActiveRecord::Base - end - - private - def authenticating_with_unauthorized_record? - !unauthorized_record.nil? - end - - def validate_by_unauthorized_record - self.attempted_record = unauthorized_record - end - end - end -end \ No newline at end of file diff --git a/lib/authlogic/session/validation.rb b/lib/authlogic/session/validation.rb deleted file mode 100644 index 66cfb4a1..00000000 --- a/lib/authlogic/session/validation.rb +++ /dev/null @@ -1,82 +0,0 @@ -module Authlogic - module Session - # Responsible for session validation - module Validation - # The errors in Authlogic work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. Use it the same way: - # - # class UserSession - # validate :check_if_awesome - # - # private - # def check_if_awesome - # errors.add(:login, "must contain awesome") if login && !login.include?("awesome") - # errors.add(:base, "You must be awesome to log in") unless attempted_record.awesome? - # end - # end - class Errors < (defined?(::ActiveModel) ? ::ActiveModel::Errors : ::ActiveRecord::Errors) - unless defined?(::ActiveModel) - def [](key) - value = super - value.is_a?(Array) ? value : [value].compact - end - end - end - - # You should use this as a place holder for any records that you find during validation. The main reason for this is to - # allow other modules to use it if needed. Take the failed_login_count feature, it needs this in order to increase - # the failed login count. - def attempted_record - @attempted_record - end - - # See attempted_record - def attempted_record=(value) - @attempted_record = value - end - - # The errors in Authlogic work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. - # Use it the same way: - # - # === Example - # - # class UserSession - # before_validation :check_if_awesome - # - # private - # def check_if_awesome - # errors.add(:login, "must contain awesome") if login && !login.include?("awesome") - # errors.add(:base, "You must be awesome to log in") unless attempted_record.awesome? - # end - # end - def errors - @errors ||= Errors.new(self) - end - - # Determines if the information you provided for authentication is valid or not. If there is - # a problem with the information provided errors will be added to the errors object and this - # method will return false. - def valid? - errors.clear - self.attempted_record = nil - - before_validation - new_session? ? before_validation_on_create : before_validation_on_update - validate - ensure_authentication_attempted - - if errors.size == 0 - new_session? ? after_validation_on_create : after_validation_on_update - after_validation - end - - save_record(attempted_record) - errors.size == 0 - end - - private - def ensure_authentication_attempted - errors.add(:base, I18n.t('error_messages.no_authentication_details', :default => "You did not provide any details for authentication.")) if errors.empty? && attempted_record.nil? - end - end - end -end diff --git a/lib/authlogic/test_case.rb b/lib/authlogic/test_case.rb index c2f89f76..067c4b33 100644 --- a/lib/authlogic/test_case.rb +++ b/lib/authlogic/test_case.rb @@ -1,12 +1,17 @@ -require File.dirname(__FILE__) + "/test_case/rails_request_adapter" -require File.dirname(__FILE__) + "/test_case/mock_cookie_jar" -require File.dirname(__FILE__) + "/test_case/mock_controller" -require File.dirname(__FILE__) + "/test_case/mock_logger" -require File.dirname(__FILE__) + "/test_case/mock_request" +# frozen_string_literal: true +require_relative "test_case/rails_request_adapter" +require_relative "test_case/mock_api_controller" +require_relative "test_case/mock_cookie_jar" +require_relative "test_case/mock_controller" +require_relative "test_case/mock_logger" +require_relative "test_case/mock_request" + +# :nodoc: module Authlogic - # This module is a collection of methods and classes that help you easily test Authlogic. In fact, - # I use these same tools to test the internals of Authlogic. + # This module is a collection of methods and classes that help you easily test + # Authlogic. In fact, I use these same tools to test the internals of + # Authlogic. # # === The quick and dirty # @@ -18,72 +23,88 @@ module Authlogic # # === Setting up # - # Authlogic comes with some simple testing tools. To get these, you need to first require Authlogic's TestCase. If - # you are doing this in a rails app, you would require this file at the top of your test_helper.rb file: + # Authlogic comes with some simple testing tools. To get these, you need to + # first require Authlogic's TestCase. If you are doing this in a rails app, + # you would require this file at the top of your test_helper.rb file: # # require "authlogic/test_case" # - # If you are using Test::Unit::TestCase, the standard testing library that comes with ruby, then you can skip this next part. - # If you are not, you need to include the Authlogic::TestCase into your testing suite as follows: + # If you are using Test::Unit::TestCase, the standard testing library that + # comes with ruby, then you can skip this next part. If you are not, you need + # to include the Authlogic::TestCase into your testing suite as follows: # # include Authlogic::TestCase # - # Now that everything is ready to go, let's move onto actually testing. Here is the basic idea behind testing: + # Now that everything is ready to go, let's move onto actually testing. Here + # is the basic idea behind testing: # - # Authlogic requires a "connection" to your controller to activate it. In the same manner that ActiveRecord requires a connection to - # your database. It can't do anything until it gets connnected. That being said, Authlogic will raise an - # Authlogic::Session::Activation::NotActivatedError any time you try to instantiate an object without a "connection". - # So before you do anything with Authlogic, you need to activate / connect Authlogic. Let's walk through how to do this in tests: + # Authlogic requires a "connection" to your controller to activate it. In the + # same manner that ActiveRecord requires a connection to your database. It + # can't do anything until it gets connected. That being said, Authlogic will + # raise an Authlogic::Session::Activation::NotActivatedError any time you try + # to instantiate an object without a "connection". So before you do anything + # with Authlogic, you need to activate / connect Authlogic. Let's walk through + # how to do this in tests: # # === Fixtures / Factories # - # Creating users via fixtures / factories is easy. Here's an example of a fixture: + # Creating users via fixtures / factories is easy. Here's an example of a + # fixture: # # ben: # email: whatever@whatever.com # password_salt: <%= salt = Authlogic::Random.hex_token %> - # crypted_password: <%= Authlogic::CryptoProviders::Sha512.encrypt("benrocks" + salt) %> + # crypted_password: <%= Authlogic::CryptoProviders::SCrypt.encrypt("benrocks" + salt) %> # persistence_token: <%= Authlogic::Random.hex_token %> # single_access_token: <%= Authlogic::Random.friendly_token %> # perishable_token: <%= Authlogic::Random.friendly_token %> # - # Notice the crypted_password value. Just supplement that with whatever crypto provider you are using, if you are not using the default. + # Notice the crypted_password value. Just supplement that with whatever crypto + # provider you are using, if you are not using the default. # # === Functional tests # - # Activating Authlogic isn't a problem here, because making a request will activate Authlogic for you. The problem is - # logging users in so they can access restricted areas. Solving this is simple, just do this: + # Activating Authlogic isn't a problem here, because making a request will + # activate Authlogic for you. The problem is logging users in so they can + # access restricted areas. Solving this is simple, just do this: # # setup :activate_authlogic # - # For those of you unfamiliar with TestUnit, the setup method bascially just executes a method before any test is ran. - # It is essentially "setting up" your tests. + # For those of you unfamiliar with TestUnit, the setup method basically just + # executes a method before any test is ran. It is essentially "setting up" + # your tests. # # Once you have done this, just log users in like usual: # # UserSession.create(users(:whomever)) # # access my restricted area here # - # Do this before you make your request and it will act as if that user is logged in. + # Do this before you make your request and it will act as if that user is + # logged in. # # === Integration tests # - # Again, just like functional tests, you don't have to do anything. As soon as you make a request, Authlogic will be - # conntected. If you want to activate Authlogic before making a request follow the same steps described in the + # Again, just like functional tests, you don't have to do anything. As soon as + # you make a request, Authlogic will be connected. If you want to activate + # Authlogic before making a request follow the same steps described in the # "functional tests" section above. It works in the same manner. # # === Unit tests # - # The only time you need to do any trickiness here is if you want to test Authlogic models. Maybe you added some custom - # code or methods in your Authlogic models. Maybe you are writing a plugin or a library that extends Authlogic. + # The only time you need to do any trickiness here is if you want to test + # Authlogic models. Maybe you added some custom code or methods in your + # Authlogic models. Maybe you are writing a plugin or a library that extends + # Authlogic. + # + # That being said, in this environment there is no controller. So you need to + # use a "mock" controller. Something that looks like a controller, acts like a + # controller, but isn't a "real" controller. You are essentially connecting + # Authlogic to your "mock" controller, then you can test off of the mock + # controller to make sure everything is functioning properly. # - # That being said, in this environment there is no controller. So you need to use a "mock" controller. Something - # that looks like a controller, acts like a controller, but isn't a "real" controller. You are essentially connecting - # Authlogic to your "mock" controller, then you can test off of the mock controller to make sure everything is functioning - # properly. - # - # I use a mock controller to test Authlogic myself. It's part of the Authlogic library that you can easily use. It's as simple - # as functional and integration tests. Just do the following: + # I use a mock controller to test Authlogic myself. It's part of the Authlogic + # library that you can easily use. It's as simple as functional and + # integration tests. Just do the following: # # setup :activate_authlogic # @@ -94,27 +115,101 @@ module Authlogic # assert UserSession.create(ben) # assert_equal controller.session["user_credentials"], ben.persistence_token # - # See how I am checking that Authlogic is interacting with the controller properly? That's the idea here. + # See how I am checking that Authlogic is interacting with the controller + # properly? That's the idea here. + # + # === Testing with Rails 5 + # + # Rails 5 has [deprecated classic controller tests](https://goo.gl/4zmt6y). + # Controller tests now inherit from `ActionDispatch::IntegrationTest` making + # them plain old integration tests now. You have two options for testing + # AuthLogic in Rails 5: + # + # * Add the `rails-controller-testing` gem to bring back the original + # controller testing usage + # * Go full steam ahead with integration testing and actually log a user in + # by submitting a form in the integration test. + # + # Naturally DHH recommends the second method and this is + # [what he does in his own tests](https://goo.gl/Ar6p0u). This is useful + # for testing not only AuthLogic itself (submitting login credentials to a + # UserSessionsController, for example) but any controller action that is + # behind a login wall. Add a helper method and use that before testing your + # actual controller action: + # + # # test/test_helper.rb + # def login(user) + # post user_sessions_url, :params => { :email => user.email, :password => 'password' } + # end + # + # # test/controllers/posts_controller_test.rb + # test "#create requires a user to be logged in + # post posts_url, :params => { :body => 'Lorem ipsum' } + # + # assert_redirected_to new_user_session_url + # end + # + # test "#create lets a logged in user create a new post" do + # login(users(:admin)) + # + # assert_difference 'Posts.count' do + # post posts_url, :params => { :body => 'Lorem ipsum' } + # end + # + # assert_redirected_to posts_url + # end + # + # You still have access to the `session` helper in an integration test and so + # you can still test to see if a user is logged in. A couple of helper methods + # might look like: + # + # # test/test_helper.rb + # def assert_logged_in + # assert session[:user_credentials].present? + # end + # + # def assert_not_logged_in + # assert session[:user_credentials].blank? + # end + # + # # test/user_sessions_controller_test.rb + # test "#create logs in a user" do + # login(users(:admin)) + # + # assert_logged_in + # end module TestCase - # Activates authlogic so that you can use it in your tests. You should call this method in your test's setup. Ex: + def initialize(*args) + @request = nil + super + end + + # Activates authlogic so that you can use it in your tests. You should call + # this method in your test's setup. Ex: # # setup :activate_authlogic def activate_authlogic - if @request && ! @request.respond_to?(:params) + if @request && !@request.respond_to?(:params) class <<@request alias_method :params, :parameters end end - Authlogic::Session::Base.controller = (@request && Authlogic::TestCase::RailsRequestAdapter.new(@request)) || controller + Authlogic::Session::Base.controller = @request && + Authlogic::TestCase::RailsRequestAdapter.new(@request) || + controller end - - # The Authlogic::TestCase::MockController object passed to Authlogic to activate it. You can access this in your test. - # See the module description for an example. + + # The Authlogic::TestCase::MockController object passed to Authlogic to + # activate it. You can access this in your test. See the module description + # for an example. def controller @controller ||= Authlogic::TestCase::MockController.new end end - + + # TODO: Why are these lines inside the `Authlogic` module? Should be outside? ::Test::Unit::TestCase.send(:include, TestCase) if defined?(::Test::Unit::TestCase) + ::MiniTest::Unit::TestCase.send(:include, TestCase) if defined?(::MiniTest::Unit::TestCase) + ::MiniTest::Test.send(:include, TestCase) if defined?(::MiniTest::Test) end diff --git a/lib/authlogic/test_case/mock_api_controller.rb b/lib/authlogic/test_case/mock_api_controller.rb new file mode 100644 index 00000000..3fe4e38e --- /dev/null +++ b/lib/authlogic/test_case/mock_api_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Authlogic + module TestCase + # Basically acts like an API controller but doesn't do anything. + # Authlogic can interact with this, do it's thing and then you can look at + # the controller object to see if anything changed. + class MockAPIController < ControllerAdapters::AbstractAdapter + attr_writer :request_content_type + + def initialize + end + + # Expected API controller has no cookies method. + undef :cookies + + def cookie_domain + nil + end + + def logger + @logger ||= MockLogger.new + end + + def params + @params ||= {} + end + + def request + @request ||= MockRequest.new(self) + end + + def request_content_type + @request_content_type ||= "text/html" + end + + def session + @session ||= {} + end + + # If method is defined, it causes below behavior... + # controller = Authlogic::ControllerAdapters::RailsAdapter.new( + # Authlogic::TestCase::MockAPIController.new + # ) + # controller.responds_to_single_access_allowed? #=> true + # controller.single_access_allowed? + # #=> NoMethodError: undefined method `single_access_allowed?' for nil:NilClass + # + undef :single_access_allowed? + end + end +end diff --git a/lib/authlogic/test_case/mock_controller.rb b/lib/authlogic/test_case/mock_controller.rb index bde7bed8..f1dcc1c4 100644 --- a/lib/authlogic/test_case/mock_controller.rb +++ b/lib/authlogic/test_case/mock_controller.rb @@ -1,45 +1,58 @@ +# frozen_string_literal: true + module Authlogic module TestCase - # Basically acts like a controller but doesn't do anything. Authlogic can interact with this, do it's thing and then you - # can look at the controller object to see if anything changed. + # Basically acts like a controller but doesn't do anything. Authlogic can interact + # with this, do it's thing and then you can look at the controller object to see if + # anything changed. class MockController < ControllerAdapters::AbstractAdapter - attr_accessor :http_user, :http_password + attr_accessor :http_user, :http_password, :realm attr_writer :request_content_type - + def initialize end - - def authenticate_with_http_basic(&block) + + def authenticate_with_http_basic yield http_user, http_password end - + + def authenticate_or_request_with_http_basic(realm = "DefaultRealm") + self.realm = realm + @http_auth_requested = true + yield http_user, http_password + end + def cookies @cookies ||= MockCookieJar.new end - + def cookie_domain nil end - + def logger @logger ||= MockLogger.new end - + def params @params ||= {} end - + def request - @request ||= MockRequest.new(controller) + @request ||= MockRequest.new(self) end - + def request_content_type @request_content_type ||= "text/html" end - + def session @session ||= {} end + + def http_auth_requested? + @http_auth_requested ||= false + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/test_case/mock_cookie_jar.rb b/lib/authlogic/test_case/mock_cookie_jar.rb index 99c38e97..ce1e3af2 100644 --- a/lib/authlogic/test_case/mock_cookie_jar.rb +++ b/lib/authlogic/test_case/mock_cookie_jar.rb @@ -1,14 +1,109 @@ +# frozen_string_literal: true + module Authlogic module TestCase + # A mock of `ActionDispatch::Cookies::CookieJar`. + # See action_dispatch/middleware/cookies.rb class MockCookieJar < Hash # :nodoc: + attr_accessor :set_cookies + def [](key) hash = super hash && hash[:value] end - - def delete(key, options = {}) + + # @param options - "the cookie's value [usually a string] or a hash of + # options as documented above [in action_dispatch/middleware/cookies.rb]" + def []=(key, options) + opt = cookie_options_to_hash(options) + (@set_cookies ||= {})[key.to_s] = opt + super(key, opt) + end + + def delete(key, _options = {}) super(key) end + + def signed + @signed ||= MockSignedCookieJar.new(self) + end + + def encrypted + @encrypted ||= MockEncryptedCookieJar.new(self) + end + + private + + # @api private + def cookie_options_to_hash(options) + if options.is_a?(Hash) + options + else + { value: options } + end + end + end + + # A mock of `ActionDispatch::Cookies::SignedKeyRotatingCookieJar` + # + # > .. a jar that'll automatically generate a signed representation of + # > cookie value and verify it when reading from the cookie again. + # > actionpack/lib/action_dispatch/middleware/cookies.rb + class MockSignedCookieJar < MockCookieJar + attr_reader :parent_jar # helper for testing + + def initialize(parent_jar) + @parent_jar = parent_jar + parent_jar.each { |k, v| self[k] = v } + end + + def [](val) + signed_message = @parent_jar[val] + if signed_message + payload, signature = signed_message.split("--") + raise "Invalid signature" unless Digest::SHA1.hexdigest(payload) == signature + payload + end + end + + def []=(key, options) + opt = cookie_options_to_hash(options) + opt[:value] = "#{opt[:value]}--#{Digest::SHA1.hexdigest opt[:value]}" + @parent_jar[key] = opt + end + end + + # Which ActionDispatch class is this a mock of? + # TODO: Document as with other mocks above. + class MockEncryptedCookieJar < MockCookieJar + attr_reader :parent_jar # helper for testing + + def initialize(parent_jar) + @parent_jar = parent_jar + parent_jar.each { |k, v| self[k] = v } + end + + def [](val) + encrypted_message = @parent_jar[val] + if encrypted_message + self.class.decrypt(encrypted_message) + end + end + + def []=(key, options) + opt = cookie_options_to_hash(options) + opt[:value] = self.class.encrypt(opt[:value]) + @parent_jar[key] = opt + end + + # simple caesar cipher for testing + def self.encrypt(str) + str.unpack("U*").map(&:succ).pack("U*") + end + + def self.decrypt(str) + str.unpack("U*").map(&:pred).pack("U*") + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/test_case/mock_logger.rb b/lib/authlogic/test_case/mock_logger.rb index b8d7c3d1..30bfd5fe 100644 --- a/lib/authlogic/test_case/mock_logger.rb +++ b/lib/authlogic/test_case/mock_logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Authlogic module TestCase # Simple class to replace real loggers, so that we can raise any errors being logged. @@ -7,4 +9,4 @@ def error(message) end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/test_case/mock_request.rb b/lib/authlogic/test_case/mock_request.rb index 84112fa9..6848d17f 100644 --- a/lib/authlogic/test_case/mock_request.rb +++ b/lib/authlogic/test_case/mock_request.rb @@ -1,19 +1,35 @@ +# frozen_string_literal: true + module Authlogic module TestCase class MockRequest # :nodoc: attr_accessor :controller - + def initialize(controller) self.controller = controller end - - def remote_ip - (controller && controller.respond_to?(:env) && controller.env.is_a?(Hash) && controller.env['REMOTE_ADDR']) || "1.1.1.1" + + def env + @env ||= { + ControllerAdapters::AbstractAdapter::ENV_SESSION_OPTIONS => {} + } end - + + def format + controller.request_content_type if controller.respond_to? :request_content_type + end + + def ip + controller&.respond_to?(:env) && + controller.env.is_a?(Hash) && + controller.env["REMOTE_ADDR"] || + "1.1.1.1" + end + private - def method_missing(*args, &block) - end + + def method_missing(*args, &block) + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/test_case/rails_request_adapter.rb b/lib/authlogic/test_case/rails_request_adapter.rb index 2beb2ef7..4b0c6000 100644 --- a/lib/authlogic/test_case/rails_request_adapter.rb +++ b/lib/authlogic/test_case/rails_request_adapter.rb @@ -1,30 +1,39 @@ +# frozen_string_literal: true + module Authlogic module TestCase - # Adapts authlogic to work with the @request object when testing. This way Authlogic can set cookies and what not before - # a request is made, ultimately letting you log in users in functional tests. + # Adapts authlogic to work with the @request object when testing. This way Authlogic + # can set cookies and what not before a request is made, ultimately letting you log in + # users in functional tests. class RailsRequestAdapter < ControllerAdapters::AbstractAdapter def authenticate_with_http_basic(&block) end - + def cookies new_cookies = MockCookieJar.new super.each do |key, value| - new_cookies[key] = value[:value] + new_cookies[key] = cookie_value(value) end new_cookies end - + def cookie_domain nil end - + def request @request ||= MockRequest.new(controller) end - + def request_content_type request.format.to_s end + + private + + def cookie_value(value) + value.is_a?(Hash) ? value[:value] : value + end end end -end \ No newline at end of file +end diff --git a/lib/authlogic/version.rb b/lib/authlogic/version.rb new file mode 100644 index 00000000..debf43e4 --- /dev/null +++ b/lib/authlogic/version.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# :nodoc: +module Authlogic + # Returns a `::Gem::Version`, the version number of the authlogic gem. + # + # It is preferable for a library to provide a `gem_version` method, rather + # than a `VERSION` string, because `::Gem::Version` is easier to use in a + # comparison. + # + # We cannot return a frozen `Version`, because rubygems will try to modify it. + # https://github.com/binarylogic/authlogic/pull/590 + # + # Added in 4.0.0 + # + # @api public + def self.gem_version + ::Gem::Version.new("6.5.0") + end +end diff --git a/rails/init.rb b/rails/init.rb deleted file mode 100644 index 5b6d0f7f..00000000 --- a/rails/init.rb +++ /dev/null @@ -1 +0,0 @@ -require "authlogic" \ No newline at end of file diff --git a/shoulda_macros/authlogic.rb b/shoulda_macros/authlogic.rb deleted file mode 100644 index 2ec8d77e..00000000 --- a/shoulda_macros/authlogic.rb +++ /dev/null @@ -1,69 +0,0 @@ -# Test::Unit -# Place this file into your test/shoulda_macros directory -# -# Example: -# -# class UserTest -# should_have_authlogic -# end -# -# Rspec -# Place this file into your spec/support/shoulda directory -# -# Example: -# -# describe User do -# it { should have_authlogic } -# end - -module Authlogic - module Shoulda - - module Matchers - def have_authlogic - HaveAuthlogic.new - end - alias_method :be_authentic, :have_authlogic - - class HaveAuthlogic - - def matches?(subject) - subject.respond_to?(:password=) && subject.respond_to?(:valid_password?) - end - - def failure_message - "Add the line 'acts_as_authentic' to your model" - end - - def description - "have Authlogic" - end - end - - end - - module Macros - include Matchers - - def should_have_authlogic - klass = described_type rescue model_class - matcher = HaveAuthlogic.new - - should matcher.description do - assert matcher.matches?(klass.new), matcher.failure_message - end - end - alias_method :should_be_authentic, :should_have_authlogic - - end - - end -end - -if defined? Spec - Spec::Runner.configure do |config| - config.include(Authlogic::Shoulda::Matchers) - end -else - Test::Unit::TestCase.class_eval { extend Authlogic::Shoulda::Macros } -end diff --git a/test/acts_as_authentic_test/base_test.rb b/test/acts_as_authentic_test/base_test.rb index 27293178..0a4fef25 100644 --- a/test/acts_as_authentic_test/base_test.rb +++ b/test/acts_as_authentic_test/base_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class BaseTest < ActiveSupport::TestCase @@ -8,11 +10,28 @@ def test_acts_as_authentic end end end - + def test_acts_as_authentic_with_old_config assert_raise(ArgumentError) do User.acts_as_authentic({}) end end + + def test_acts_as_authentic_with_no_table_raise_on_model_setup_error_default + klass = Class.new(ActiveRecord::Base) + assert_nothing_raised do + klass.acts_as_authentic + end + end + + def test_acts_as_authentic_with_no_table_raise_on_model_setup_error_enabled + klass = Class.new(ActiveRecord::Base) + e = assert_raises Authlogic::ModelSetupError do + klass.acts_as_authentic do |c| + c.raise_on_model_setup_error = true + end + end + refute e.message.empty? + end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/email_test.rb b/test/acts_as_authentic_test/email_test.rb index ad7d8518..f3a93907 100644 --- a/test/acts_as_authentic_test/email_test.rb +++ b/test/acts_as_authentic_test/email_test.rb @@ -1,101 +1,32 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class EmailTest < ActiveSupport::TestCase def test_email_field_config assert_equal :email, User.email_field assert_equal :email, Employee.email_field - + User.email_field = :nope assert_equal :nope, User.email_field User.email_field :email assert_equal :email, User.email_field end - - def test_validate_email_field_config - assert User.validate_email_field - assert Employee.validate_email_field - - User.validate_email_field = false - assert !User.validate_email_field - User.validate_email_field true - assert User.validate_email_field - end - - def test_validates_length_of_email_field_options_config - assert_equal({:within => 6..100}, User.validates_length_of_email_field_options) - assert_equal({:within => 6..100}, Employee.validates_length_of_email_field_options) - - User.validates_length_of_email_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_length_of_email_field_options) - User.validates_length_of_email_field_options({:within => 6..100}) - assert_equal({:within => 6..100}, User.validates_length_of_email_field_options) - end - - def test_validates_format_of_email_field_options_config - default = {:with => Authlogic::Regex.email, :message => I18n.t('error_messages.email_invalid', :default => "should look like an email address.")} - assert_equal default, User.validates_format_of_email_field_options - assert_equal default, Employee.validates_format_of_email_field_options - - User.validates_format_of_email_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_format_of_email_field_options) - User.validates_format_of_email_field_options default - assert_equal default, User.validates_format_of_email_field_options - end - - def test_validates_uniqueness_of_email_field_options_config - default = {:case_sensitive => false, :scope => Employee.validations_scope, :if => "#{Employee.email_field}_changed?".to_sym} - assert_equal default, Employee.validates_uniqueness_of_email_field_options - - Employee.validates_uniqueness_of_email_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, Employee.validates_uniqueness_of_email_field_options) - Employee.validates_uniqueness_of_email_field_options default - assert_equal default, Employee.validates_uniqueness_of_email_field_options - end - - def test_validates_length_of_email_field - u = User.new - u.email = "a@a.a" - assert !u.valid? - assert u.errors[:email].size > 0 - - u.email = "a@a.com" - assert !u.valid? - assert u.errors[:email].size == 0 - end - - def test_validates_format_of_email_field - u = User.new - u.email = "aaaaaaaaaaaaa" - u.valid? - assert u.errors[:email].size > 0 - - u.email = "a@a.com" - u.valid? - assert u.errors[:email].size == 0 - - u.email = "damien+test1...etc..@mydomain.com" - u.valid? - assert u.errors[:email].size == 0 - - u.email = "dakota.dux+1@gmail.com" - u.valid? - assert u.errors[:email].size == 0 - end - - def test_validates_uniqueness_of_email_field - u = User.new - u.email = "bjohnson@binarylogic.com" - assert !u.valid? - assert u.errors[:email].size > 0 - - u.email = "BJOHNSON@binarylogic.com" - assert !u.valid? - assert u.errors[:email].size > 0 - - u.email = "a@a.com" - assert !u.valid? - assert u.errors[:email].size == 0 + + def test_deferred_error_message_translation + # ensure we successfully loaded the test locale + assert I18n.available_locales.include?(:lol), "Test locale failed to load" + + I18n.with_locale("lol") do + cat = User.new + cat.email = "meow" + cat.validate + assert_includes( + cat.errors[:email], + I18n.t("authlogic.error_messages.email_invalid") + ) + end end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/logged_in_status_test.rb b/test/acts_as_authentic_test/logged_in_status_test.rb index 4092818a..d08c9891 100644 --- a/test/acts_as_authentic_test/logged_in_status_test.rb +++ b/test/acts_as_authentic_test/logged_in_status_test.rb @@ -1,36 +1,64 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class LoggedInStatusTest < ActiveSupport::TestCase + ERROR_MSG = "Multiple calls to %s should result in different relations" + def test_logged_in_timeout_config assert_equal 10.minutes.to_i, User.logged_in_timeout assert_equal 10.minutes.to_i, Employee.logged_in_timeout - + User.logged_in_timeout = 1.hour assert_equal 1.hour.to_i, User.logged_in_timeout User.logged_in_timeout 10.minutes assert_equal 10.minutes.to_i, User.logged_in_timeout end - + def test_named_scope_logged_in + # Testing that the scope returned differs, because the time it was called should be + # slightly different. This is an attempt to make sure the scope is lambda wrapped + # so that it is re-evaluated every time its called. My biggest concern is that the + # test happens so fast that the test fails... I just don't know a better way to test it! + + # for rails 5 I've changed the where_values to to_sql to compare + + query1 = User.logged_in.to_sql + sleep 0.1 + query2 = User.logged_in.to_sql + assert query1 != query2, ERROR_MSG % "#logged_in" + assert_equal 0, User.logged_in.count - User.first.update_attribute(:last_request_at, Time.now) + user = User.first + user.last_request_at = Time.now + user.current_login_at = Time.now + user.save! assert_equal 1, User.logged_in.count end - + def test_named_scope_logged_out - assert_equal 2, User.logged_out.count + # Testing that the scope returned differs, because the time it was called should be + # slightly different. This is an attempt to make sure the scope is lambda wrapped + # so that it is re-evaluated every time its called. My biggest concern is that the + # test happens so fast that the test fails... I just don't know a better way to test it! + + # for rails 5 I've changed the where_values to to_sql to compare + + assert User.logged_in.to_sql != User.logged_out.to_sql, ERROR_MSG % "#logged_out" + + assert_equal 3, User.logged_out.count User.first.update_attribute(:last_request_at, Time.now) - assert_equal 1, User.logged_out.count + assert_equal 2, User.logged_out.count end - + def test_logged_in_logged_out u = User.first - assert !u.logged_in? + refute u.logged_in? assert u.logged_out? u.last_request_at = Time.now assert u.logged_in? - assert !u.logged_out? + refute u.logged_out? end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/login_test.rb b/test/acts_as_authentic_test/login_test.rb index 6159477e..0972ab52 100644 --- a/test/acts_as_authentic_test/login_test.rb +++ b/test/acts_as_authentic_test/login_test.rb @@ -1,109 +1,34 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest - class LoginTest < ActiveSupport::TestCase + # Miscellaneous tests for configuration options related to the `login_field`. + class MiscellaneousLoginTest < ActiveSupport::TestCase def test_login_field_config assert_equal :login, User.login_field assert_nil Employee.login_field - + User.login_field = :nope assert_equal :nope, User.login_field User.login_field :login assert_equal :login, User.login_field end - - def test_validate_login_field_config - assert User.validate_login_field - assert Employee.validate_login_field - - User.validate_login_field = false - assert !User.validate_login_field - User.validate_login_field true - assert User.validate_login_field - end - - def test_validates_length_of_login_field_options_config - assert_equal({:within => 3..100}, User.validates_length_of_login_field_options) - assert_equal({:within => 3..100}, Employee.validates_length_of_login_field_options) - - User.validates_length_of_login_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_length_of_login_field_options) - User.validates_length_of_login_field_options({:within => 3..100}) - assert_equal({:within => 3..100}, User.validates_length_of_login_field_options) - end - - def test_validates_format_of_login_field_options_config - default = {:with => /\A\w[\w\.+\-_@ ]+$/, :message => I18n.t('error_messages.login_invalid', :default => "should use only letters, numbers, spaces, and .-_@ please.")} - assert_equal default, User.validates_format_of_login_field_options - assert_equal default, Employee.validates_format_of_login_field_options - - User.validates_format_of_login_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_format_of_login_field_options) - User.validates_format_of_login_field_options default - assert_equal default, User.validates_format_of_login_field_options - end - - def test_validates_uniqueness_of_login_field_options_config - default = {:case_sensitive => false, :scope => User.validations_scope, :if => "#{User.login_field}_changed?".to_sym} - assert_equal default, User.validates_uniqueness_of_login_field_options - - User.validates_uniqueness_of_login_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_uniqueness_of_login_field_options) - User.validates_uniqueness_of_login_field_options default - assert_equal default, User.validates_uniqueness_of_login_field_options - end - - def test_validates_length_of_login_field - u = User.new - u.login = "a" - assert !u.valid? - assert u.errors[:login].size > 0 - - u.login = "aaaaaaaaaa" - assert !u.valid? - assert u.errors[:login].size == 0 - end - - def test_validates_format_of_login_field - u = User.new - u.login = "fdsf@^&*" - assert !u.valid? - assert u.errors[:login].size > 0 - - u.login = "fdsfdsfdsfdsfs" - assert !u.valid? - assert u.errors[:login].size == 0 - - u.login = "dakota.dux+1@gmail.com" - assert !u.valid? - assert u.errors[:login].size == 0 - end - - def test_validates_uniqueness_of_login_field - u = User.new - u.login = "bjohnson" - assert !u.valid? - assert u.errors[:login].size > 0 - - u.login = "BJOHNSON" - assert !u.valid? - assert u.errors[:login].size > 0 - - u.login = "fdsfdsf" - assert !u.valid? - assert u.errors[:login].size == 0 - end - + def test_find_by_smart_case_login_field + # `User` is configured to be case-sensitive. (It has a case-sensitive + # uniqueness validation) ben = users(:ben) assert_equal ben, User.find_by_smart_case_login_field("bjohnson") - assert_equal ben, User.find_by_smart_case_login_field("BJOHNSON") - assert_equal ben, User.find_by_smart_case_login_field("Bjohnson") - + assert_nil User.find_by_smart_case_login_field("BJOHNSON") + assert_nil User.find_by_smart_case_login_field("Bjohnson") + + # Unlike `User`, `Employee` does not have a uniqueness validation. In + # the absence of such, authlogic performs a case-insensitive query. drew = employees(:drew) assert_equal drew, Employee.find_by_smart_case_login_field("dgainor@binarylogic.com") assert_equal drew, Employee.find_by_smart_case_login_field("Dgainor@binarylogic.com") assert_equal drew, Employee.find_by_smart_case_login_field("DGAINOR@BINARYLOGIC.COM") end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/magic_columns_test.rb b/test/acts_as_authentic_test/magic_columns_test.rb index d9fcfa49..1d07b69b 100644 --- a/test/acts_as_authentic_test/magic_columns_test.rb +++ b/test/acts_as_authentic_test/magic_columns_test.rb @@ -1,27 +1,29 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class MagicColumnsTest < ActiveSupport::TestCase def test_validates_numericality_of_login_count u = User.new u.login_count = -1 - assert !u.valid? - assert u.errors[:login_count].size > 0 - + refute u.valid? + refute u.errors[:login_count].empty? + u.login_count = 0 - assert !u.valid? - assert u.errors[:login_count].size == 0 + refute u.valid? + assert u.errors[:login_count].empty? end - + def test_validates_numericality_of_failed_login_count u = User.new u.failed_login_count = -1 - assert !u.valid? - assert u.errors[:failed_login_count].size > 0 - + refute u.valid? + refute u.errors[:failed_login_count].empty? + u.failed_login_count = 0 - assert !u.valid? - assert u.errors[:failed_login_count].size == 0 + refute u.valid? + assert u.errors[:failed_login_count].empty? end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/password_test.rb b/test/acts_as_authentic_test/password_test.rb index 3ae95a62..e13c76dd 100644 --- a/test/acts_as_authentic_test/password_test.rb +++ b/test/acts_as_authentic_test/password_test.rb @@ -1,154 +1,72 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class PasswordTest < ActiveSupport::TestCase + # If test_human_name is executed after test_i18n_of_human_name the test will fail. + i_suck_and_my_tests_are_order_dependent! + def test_crypted_password_field_config assert_equal :crypted_password, User.crypted_password_field assert_equal :crypted_password, Employee.crypted_password_field - + User.crypted_password_field = :nope assert_equal :nope, User.crypted_password_field User.crypted_password_field :crypted_password assert_equal :crypted_password, User.crypted_password_field end - + def test_password_salt_field_config assert_equal :password_salt, User.password_salt_field assert_equal :password_salt, Employee.password_salt_field - + User.password_salt_field = :nope assert_equal :nope, User.password_salt_field User.password_salt_field :password_salt assert_equal :password_salt, User.password_salt_field end - + def test_ignore_blank_passwords_config assert User.ignore_blank_passwords assert Employee.ignore_blank_passwords - + User.ignore_blank_passwords = false - assert !User.ignore_blank_passwords + refute User.ignore_blank_passwords User.ignore_blank_passwords true assert User.ignore_blank_passwords end - + def test_check_passwords_against_database assert User.check_passwords_against_database User.check_passwords_against_database = false - assert !User.check_passwords_against_database + refute User.check_passwords_against_database User.check_passwords_against_database true assert User.check_passwords_against_database end - - def test_validate_password_field_config - assert User.validate_password_field - assert Employee.validate_password_field - - User.validate_password_field = false - assert !User.validate_password_field - User.validate_password_field true - assert User.validate_password_field - end - - def test_validates_length_of_password_field_options_config - default = {:minimum => 4, :if => :require_password?} - assert_equal default, User.validates_length_of_password_field_options - assert_equal default, Employee.validates_length_of_password_field_options - - User.validates_length_of_password_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_length_of_password_field_options) - User.validates_length_of_password_field_options default - assert_equal default, User.validates_length_of_password_field_options - end - - def test_validates_confirmation_of_password_field_options_config - default = {:if => :require_password?} - assert_equal default, User.validates_confirmation_of_password_field_options - assert_equal default, Employee.validates_confirmation_of_password_field_options - - User.validates_confirmation_of_password_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_confirmation_of_password_field_options) - User.validates_confirmation_of_password_field_options default - assert_equal default, User.validates_confirmation_of_password_field_options - end - - def test_validates_length_of_password_confirmation_field_options_config - default = {:minimum => 4, :if => :require_password?} - assert_equal default, User.validates_length_of_password_confirmation_field_options - assert_equal default, Employee.validates_length_of_password_confirmation_field_options - - User.validates_length_of_password_confirmation_field_options = {:yes => "no"} - assert_equal({:yes => "no"}, User.validates_length_of_password_confirmation_field_options) - User.validates_length_of_password_confirmation_field_options default - assert_equal default, User.validates_length_of_password_confirmation_field_options - end - + def test_crypto_provider_config - assert_equal Authlogic::CryptoProviders::Sha512, User.crypto_provider - assert_equal Authlogic::CryptoProviders::AES256, Employee.crypto_provider - - User.crypto_provider = Authlogic::CryptoProviders::BCrypt + assert_equal Authlogic::CryptoProviders::SCrypt, User.crypto_provider + silence_warnings do + User.crypto_provider = Authlogic::CryptoProviders::BCrypt + end assert_equal Authlogic::CryptoProviders::BCrypt, User.crypto_provider - User.crypto_provider Authlogic::CryptoProviders::Sha512 + silence_warnings do + User.crypto_provider = Authlogic::CryptoProviders::Sha512 + end assert_equal Authlogic::CryptoProviders::Sha512, User.crypto_provider end - + def test_transition_from_crypto_providers_config - assert_equal [], User.transition_from_crypto_providers + assert_equal [Authlogic::CryptoProviders::Sha512], User.transition_from_crypto_providers assert_equal [], Employee.transition_from_crypto_providers - + User.transition_from_crypto_providers = [Authlogic::CryptoProviders::BCrypt] assert_equal [Authlogic::CryptoProviders::BCrypt], User.transition_from_crypto_providers User.transition_from_crypto_providers [] assert_equal [], User.transition_from_crypto_providers end - - def test_validates_length_of_password - u = User.new - u.password_confirmation = "test2" - assert !u.valid? - assert u.errors[:password].size > 0 - - u.password = "test" - assert !u.valid? - assert u.errors[:password_confirmation].size == 0 - end - - def test_validates_confirmation_of_password - u = User.new - u.password = "test" - u.password_confirmation = "test2" - assert !u.valid? - assert u.errors[:password].size > 0 - - u.password_confirmation = "test" - assert !u.valid? - assert u.errors[:password].size == 0 - end - - def test_validates_length_of_password_confirmation - u = User.new - - u.password = "test" - u.password_confirmation = "" - assert !u.valid? - assert u.errors[:password_confirmation].size > 0 - - u.password_confirmation = "test" - assert !u.valid? - assert u.errors[:password_confirmation].size == 0 - - ben = users(:ben) - assert ben.valid? - - ben.password = "newpass" - assert !ben.valid? - assert ben.errors[:password_confirmation].size > 0 - - ben.password_confirmation = "newpass" - assert ben.valid? - end - + def test_password u = User.new old_password_salt = u.password_salt @@ -157,80 +75,128 @@ def test_password assert_not_equal old_password_salt, u.password_salt assert_not_equal old_crypted_password, u.crypted_password end - + def test_transitioning_password ben = users(:ben) + transition_password_to(Authlogic::CryptoProviders::BCrypt, ben) - transition_password_to(Authlogic::CryptoProviders::Sha1, ben, [Authlogic::CryptoProviders::Sha512, Authlogic::CryptoProviders::BCrypt]) - transition_password_to(Authlogic::CryptoProviders::Sha512, ben, [Authlogic::CryptoProviders::Sha1, Authlogic::CryptoProviders::BCrypt]) + transition_password_to( + Authlogic::CryptoProviders::Sha1, + ben, + [Authlogic::CryptoProviders::Sha512, Authlogic::CryptoProviders::BCrypt] + ) + transition_password_to( + Authlogic::CryptoProviders::Sha512, + ben, + [Authlogic::CryptoProviders::Sha1, Authlogic::CryptoProviders::BCrypt] + ) end - - def test_checks_password_against_database + + def test_v2_crypto_provider_transition ben = users(:ben) + + providers = [ + Authlogic::CryptoProviders::Sha512::V2, + Authlogic::CryptoProviders::MD5::V2, + Authlogic::CryptoProviders::Sha1::V2, + Authlogic::CryptoProviders::Sha256::V2 + ] + transition_password_to(providers[0], ben) + providers.each_cons(2) do |old_provider, new_provider| + transition_password_to( + new_provider, + ben, + old_provider + ) + end + end + + def test_checks_password_against_database + ben = users(:aaron) ben.password = "new pass" - assert !ben.valid_password?("new pass") - assert ben.valid_password?("benrocks") + refute ben.valid_password?("new pass") + assert ben.valid_password?("aaronrocks") end - + def test_checks_password_against_database_and_always_fails_on_new_records user = User.new user.password = "new pass" - assert !user.valid_password?("new pass") + refute user.valid_password?("new pass") end - + def test_checks_password_against_object ben = users(:ben) ben.password = "new pass" assert ben.valid_password?("new pass", false) - assert !ben.valid_password?("benrocks", false) + refute ben.valid_password?("benrocks", false) end - + def test_reset_password ben = users(:ben) old_crypted_password = ben.crypted_password old_password_salt = ben.password_salt - + # soft reset ben.reset_password assert_not_equal old_crypted_password, ben.crypted_password assert_not_equal old_password_salt, ben.password_salt - + # make sure it didn't go into the db ben.reload assert_equal old_crypted_password, ben.crypted_password assert_equal old_password_salt, ben.password_salt - + # hard reset assert ben.reset_password! assert_not_equal old_crypted_password, ben.crypted_password assert_not_equal old_password_salt, ben.password_salt - + # make sure it did go into the db ben.reload assert_not_equal old_crypted_password, ben.crypted_password assert_not_equal old_password_salt, ben.password_salt end - + + def test_reset_password_in_after_save + lumbergh = admins(:lumbergh) + old_crypted_password = lumbergh.crypted_password + lumbergh.role = "Stapler Supervisor" + lumbergh.save! + # Because his `role` changed, the `after_save` callback in admin.rb will + # reset Lumbergh's password. + assert_not_equal old_crypted_password, lumbergh.crypted_password + # Lumbergh's perishable_token has also changed, but that's not relevant + # to this test because perishable_token always changes whenever you save + # anything (unless you `disable_perishable_token_maintenance`). + end + private - def transition_password_to(crypto_provider, records, from_crypto_providers = Authlogic::CryptoProviders::Sha512) - records = [records] unless records.is_a?(Array) - User.acts_as_authentic do |c| + + def transition_password_to( + crypto_provider, + records, + from_crypto_providers = Authlogic::CryptoProviders::Sha512 + ) + records = [records] unless records.is_a?(Array) + User.acts_as_authentic do |c| + silence_warnings do c.crypto_provider = crypto_provider - c.transition_from_crypto_providers = from_crypto_providers - end - records.each do |record| - old_hash = record.crypted_password - old_persistence_token = record.persistence_token - assert record.valid_password?(password_for(record)) - assert_not_equal old_hash.to_s, record.crypted_password.to_s - assert_not_equal old_persistence_token.to_s, record.persistence_token.to_s - - old_hash = record.crypted_password - old_persistence_token = record.persistence_token - assert record.valid_password?(password_for(record)) - assert_equal old_hash.to_s, record.crypted_password.to_s - assert_equal old_persistence_token.to_s, record.persistence_token.to_s end + c.transition_from_crypto_providers = from_crypto_providers end + records.each do |record| + old_hash = record.crypted_password + old_persistence_token = record.persistence_token + assert record.valid_password?(password_for(record)) + assert_not_equal old_hash.to_s, record.crypted_password.to_s + assert_not_equal old_persistence_token.to_s, record.persistence_token.to_s + + old_hash = record.crypted_password + old_persistence_token = record.persistence_token + assert record.valid_password?(password_for(record)) + assert_equal old_hash.to_s, record.crypted_password.to_s + assert_equal old_persistence_token.to_s, record.persistence_token.to_s + end + end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/perishable_token_test.rb b/test/acts_as_authentic_test/perishable_token_test.rb index 32c95223..da485ccc 100644 --- a/test/acts_as_authentic_test/perishable_token_test.rb +++ b/test/acts_as_authentic_test/perishable_token_test.rb @@ -1,81 +1,89 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class PerishableTokenTest < ActiveSupport::TestCase def test_perishable_token_valid_for_config assert_equal 10.minutes.to_i, User.perishable_token_valid_for assert_equal 10.minutes.to_i, Employee.perishable_token_valid_for - + User.perishable_token_valid_for = 1.hour assert_equal 1.hour.to_i, User.perishable_token_valid_for User.perishable_token_valid_for 10.minutes assert_equal 10.minutes.to_i, User.perishable_token_valid_for end - + def test_disable_perishable_token_maintenance_config - assert !User.disable_perishable_token_maintenance - assert !Employee.disable_perishable_token_maintenance - + refute User.disable_perishable_token_maintenance + refute Employee.disable_perishable_token_maintenance + User.disable_perishable_token_maintenance = true assert User.disable_perishable_token_maintenance User.disable_perishable_token_maintenance false - assert !User.disable_perishable_token_maintenance + refute User.disable_perishable_token_maintenance end - + def test_validates_uniqueness_of_perishable_token u = User.new u.perishable_token = users(:ben).perishable_token - assert !u.valid? - assert u.errors[:perishable_token].size > 0 + refute u.valid? + refute u.errors[:perishable_token].empty? end - + def test_before_save_reset_perishable_token ben = users(:ben) old_perishable_token = ben.perishable_token assert ben.save assert_not_equal old_perishable_token, ben.perishable_token end - + def test_reset_perishable_token ben = users(:ben) old_perishable_token = ben.perishable_token - + assert ben.reset_perishable_token assert_not_equal old_perishable_token, ben.perishable_token - + ben.reload assert_equal old_perishable_token, ben.perishable_token - + assert ben.reset_perishable_token! assert_not_equal old_perishable_token, ben.perishable_token - + ben.reload assert_not_equal old_perishable_token, ben.perishable_token end - + def test_find_using_perishable_token ben = users(:ben) assert_equal ben, User.find_using_perishable_token(ben.perishable_token) end - + def test_find_using_perishable_token_when_perished ben = users(:ben) - ActiveRecord::Base.connection.execute("UPDATE users set updated_at = '#{1.week.ago.to_s(:db)}' where id = #{ben.id}") + ActiveRecord::Base.connection.execute( + "UPDATE users set updated_at = '#{1.week.ago.to_formatted_s(:db)}' where id = #{ben.id}" + ) assert_nil User.find_using_perishable_token(ben.perishable_token) end - - def test_find_using_perishable_token_when_perished + + def test_find_using_perishable_token_when_perished_2 User.perishable_token_valid_for = 1.minute ben = users(:ben) - ActiveRecord::Base.connection.execute("UPDATE users set updated_at = '#{2.minutes.ago.to_s(:db)}' where id = #{ben.id}") + ActiveRecord::Base.connection.execute( + "UPDATE users set updated_at = '#{2.minutes.ago.to_formatted_s(:db)}' where id = #{ben.id}" + ) assert_nil User.find_using_perishable_token(ben.perishable_token) User.perishable_token_valid_for = 10.minutes end - + def test_find_using_perishable_token_when_passing_threshold User.perishable_token_valid_for = 1.minute ben = users(:ben) - ActiveRecord::Base.connection.execute("UPDATE users set updated_at = '#{10.minutes.ago.to_s(:db)}' where id = #{ben.id}") + ActiveRecord::Base.connection.execute( + "UPDATE users set updated_at = '#{10.minutes.ago.to_formatted_s(:db)}' where id = #{ben.id}" + ) assert_nil User.find_using_perishable_token(ben.perishable_token, 5.minutes) assert_equal ben, User.find_using_perishable_token(ben.perishable_token, 20.minutes) User.perishable_token_valid_for = 10.minutes @@ -83,7 +91,7 @@ def test_find_using_perishable_token_when_passing_threshold def test_find_perishable_token_with_bang assert_raises ActiveRecord::RecordNotFound do - User.find_using_perishable_token!('some_bad_value') + User.find_using_perishable_token!("some_bad_value") end end end diff --git a/test/acts_as_authentic_test/persistence_token_test.rb b/test/acts_as_authentic_test/persistence_token_test.rb index b8dc3cb8..4bec2650 100644 --- a/test/acts_as_authentic_test/persistence_token_test.rb +++ b/test/acts_as_authentic_test/persistence_token_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class PersistenceTokenTest < ActiveSupport::TestCase @@ -8,36 +10,41 @@ def test_after_password_set_reset_persistence_token ben.password = "newpass" assert_not_equal old_persistence_token, ben.persistence_token end - + def test_after_password_verification_reset_persistence_token - ben = users(:ben) - old_persistence_token = ben.persistence_token - assert ben.valid_password?(password_for(ben)) - assert_equal old_persistence_token, ben.persistence_token - + aaron = users(:aaron) + old_persistence_token = aaron.persistence_token + + assert aaron.valid_password?(password_for(aaron)) + assert_equal old_persistence_token, aaron.reload.persistence_token + # only update it if it is nil - assert ben.update_attribute(:persistence_token, nil) - assert ben.valid_password?(password_for(ben)) - assert_not_equal old_persistence_token, ben.persistence_token + assert aaron.update_attribute(:persistence_token, nil) + assert aaron.valid_password?(password_for(aaron)) + assert_not_equal old_persistence_token, aaron.persistence_token end - + def test_before_validate_reset_persistence_token u = User.new - assert !u.valid? + refute u.valid? assert_not_nil u.persistence_token end - + def test_forget_all + UserSession.allow_http_basic_auth = true + http_basic_auth_for(users(:ben)) { UserSession.find } http_basic_auth_for(users(:zack)) { UserSession.find(:ziggity_zack) } assert UserSession.find assert UserSession.find(:ziggity_zack) User.forget_all - assert !UserSession.find - assert !UserSession.find(:ziggity_zack) + refute UserSession.find + refute UserSession.find(:ziggity_zack) end - + def test_forget + UserSession.allow_http_basic_auth = true + ben = users(:ben) zack = users(:zack) http_basic_auth_for(ben) { UserSession.find } @@ -48,8 +55,8 @@ def test_forget ben.forget! - assert !UserSession.find + refute UserSession.find assert UserSession.find(:ziggity_zack) end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/restful_authentication_test.rb b/test/acts_as_authentic_test/restful_authentication_test.rb deleted file mode 100644 index 36dd1ec2..00000000 --- a/test/acts_as_authentic_test/restful_authentication_test.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'test_helper' - -module ActsAsAuthenticTest - class RestfulAuthenticationTest < ActiveSupport::TestCase - def test_act_like_restful_authentication_config - assert !User.act_like_restful_authentication - assert !Employee.act_like_restful_authentication - - User.act_like_restful_authentication = true - assert User.act_like_restful_authentication - assert_equal Authlogic::CryptoProviders::Sha1, User.crypto_provider - assert defined?(::REST_AUTH_SITE_KEY) - assert_equal '', ::REST_AUTH_SITE_KEY - assert_equal 1, Authlogic::CryptoProviders::Sha1.stretches - - User.act_like_restful_authentication false - assert !User.act_like_restful_authentication - - User.crypto_provider = Authlogic::CryptoProviders::Sha512 - User.transition_from_crypto_providers = [] - end - - def test_transition_from_restful_authentication_config - assert !User.transition_from_restful_authentication - assert !Employee.transition_from_restful_authentication - - User.transition_from_restful_authentication = true - assert User.transition_from_restful_authentication - assert defined?(::REST_AUTH_SITE_KEY) - assert_equal '', ::REST_AUTH_SITE_KEY - assert_equal 1, Authlogic::CryptoProviders::Sha1.stretches - - User.transition_from_restful_authentication false - assert !User.transition_from_restful_authentication - - User.crypto_provider = Authlogic::CryptoProviders::Sha512 - User.transition_from_crypto_providers = [] - end - end -end \ No newline at end of file diff --git a/test/acts_as_authentic_test/session_maintenance_test.rb b/test/acts_as_authentic_test/session_maintenance_test.rb index 3ae53bb8..aaf78fd9 100644 --- a/test/acts_as_authentic_test/session_maintenance_test.rb +++ b/test/acts_as_authentic_test/session_maintenance_test.rb @@ -1,35 +1,92 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class SessionMaintenanceTest < ActiveSupport::TestCase - def test_maintain_sessions_config - assert User.maintain_sessions - User.maintain_sessions = false - assert !User.maintain_sessions - User.maintain_sessions true - assert User.maintain_sessions + def setup + User.log_in_after_create = true + User.log_in_after_password_change = true + end + + def test_log_in_after_create_config + assert User.log_in_after_create + User.log_in_after_create = false + refute User.log_in_after_create + User.log_in_after_create = true + assert User.log_in_after_create end - + + def test_log_in_after_password_change_config + assert User.log_in_after_password_change + User.log_in_after_password_change = false + refute User.log_in_after_password_change + User.log_in_after_password_change = true + assert User.log_in_after_password_change + end + def test_login_after_create - assert User.create(:login => "awesome", :password => "saweet", :password_confirmation => "saweet", :email => "awesome@awesome.com") + User.log_in_after_create = true + user = User.create( + login: "awesome", + password: "saweeeet", + password_confirmation: "saweeeet", + email: "awesome@awesome.com" + ) + assert user.persisted? assert UserSession.find + logged_in_user = UserSession.find.user + assert_equal logged_in_user, user end - + + def test_no_login_after_create + old_user = User.create( + login: "awesome", + password: "saweeeet", + password_confirmation: "saweeeet", + email: "awesome@awesome.com" + ) + User.log_in_after_create = false + user2 = User.create( + login: "awesome2", + password: "saweeeet2", + password_confirmation: "saweeeet2", + email: "awesome2@awesome.com" + ) + assert user2.persisted? + logged_in_user = UserSession.find.user + assert_not_equal logged_in_user, user2 + assert_equal logged_in_user, old_user + end + + def test_no_login_after_logged_out_create + User.log_in_after_create = false + user = User.create( + login: "awesome2", + password: "saweeeet2", + password_confirmation: "saweeeet2", + email: "awesome2@awesome.com" + ) + assert user.persisted? + assert_nil UserSession.find + end + def test_updating_session_with_failed_magic_state ben = users(:ben) ben.confirmed = false - ben.password = "newpass" - ben.password_confirmation = "newpass" + ben.password = "newpasswd" + ben.password_confirmation = "newpasswd" assert ben.save end def test_update_session_after_password_modify + User.log_in_after_password_change = true ben = users(:ben) UserSession.create(ben) old_session_key = controller.session["user_credentials"] old_cookie_key = controller.cookies["user_credentials"] - ben.password = "newpass" - ben.password_confirmation = "newpass" + ben.password = "newpasswd" + ben.password_confirmation = "newpasswd" assert ben.save assert controller.session["user_credentials"] assert controller.cookies["user_credentials"] @@ -37,6 +94,21 @@ def test_update_session_after_password_modify assert_not_equal controller.cookies["user_credentials"], old_cookie_key end + def test_no_update_session_after_password_modify + User.log_in_after_password_change = false + ben = users(:ben) + UserSession.create(ben) + old_session_key = controller.session["user_credentials"] + old_cookie_key = controller.cookies["user_credentials"] + ben.password = "newpasswd" + ben.password_confirmation = "newpasswd" + assert ben.save + assert controller.session["user_credentials"] + assert controller.cookies["user_credentials"] + assert_equal controller.session["user_credentials"], old_session_key + assert_equal controller.cookies["user_credentials"], old_cookie_key + end + def test_no_session_update_after_modify ben = users(:ben) UserSession.create(ben) @@ -47,13 +119,19 @@ def test_no_session_update_after_modify assert_equal controller.session["user_credentials"], old_session_key assert_equal controller.cookies["user_credentials"], old_cookie_key end - + def test_creating_other_user ben = users(:ben) UserSession.create(ben) old_session_key = controller.session["user_credentials"] old_cookie_key = controller.cookies["user_credentials"] - assert User.create(:login => "awesome", :password => "saweet", :password_confirmation => "saweet", :email => "awesome@saweet.com") + user = User.create( + login: "awesome", + password: "saweet", # Password is too short, user invalid + password_confirmation: "saweet", + email: "awesome@saweet.com" + ) + refute user.persisted? assert_equal controller.session["user_credentials"], old_session_key assert_equal controller.cookies["user_credentials"], old_cookie_key end @@ -64,8 +142,8 @@ def test_updating_other_user old_session_key = controller.session["user_credentials"] old_cookie_key = controller.cookies["user_credentials"] zack = users(:zack) - zack.password = "newpass" - zack.password_confirmation = "newpass" + zack.password = "newpasswd" + zack.password_confirmation = "newpasswd" assert zack.save assert_equal controller.session["user_credentials"], old_session_key assert_equal controller.cookies["user_credentials"], old_cookie_key @@ -73,12 +151,12 @@ def test_updating_other_user def test_resetting_password_when_logged_out ben = users(:ben) - assert !UserSession.find - ben.password = "newpass" - ben.password_confirmation = "newpass" + refute UserSession.find + ben.password = "newpasswd" + ben.password_confirmation = "newpasswd" assert ben.save assert UserSession.find assert_equal ben, UserSession.find.record end end -end \ No newline at end of file +end diff --git a/test/acts_as_authentic_test/single_access_test.rb b/test/acts_as_authentic_test/single_access_test.rb index 5127e3ee..1d91d654 100644 --- a/test/acts_as_authentic_test/single_access_test.rb +++ b/test/acts_as_authentic_test/single_access_test.rb @@ -1,44 +1,46 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module ActsAsAuthenticTest class SingleAccessTest < ActiveSupport::TestCase def test_change_single_access_token_with_password_config - assert !User.change_single_access_token_with_password - assert !Employee.change_single_access_token_with_password - + refute User.change_single_access_token_with_password + refute Employee.change_single_access_token_with_password + User.change_single_access_token_with_password = true assert User.change_single_access_token_with_password User.change_single_access_token_with_password false - assert !User.change_single_access_token_with_password + refute User.change_single_access_token_with_password end - + def test_validates_uniqueness_of_single_access_token u = User.new u.single_access_token = users(:ben).single_access_token - assert !u.valid? - assert u.errors[:single_access_token].size > 0 + refute u.valid? + refute u.errors[:single_access_token].empty? end - + def test_before_validation_reset_single_access_token u = User.new - assert !u.valid? + refute u.valid? assert_not_nil u.single_access_token end - + def test_after_password_set_reset_single_access_token User.change_single_access_token_with_password = true - + ben = users(:ben) old_single_access_token = ben.single_access_token ben.password = "new_pass" assert_not_equal old_single_access_token, ben.single_access_token - + User.change_single_access_token_with_password = false end - + def test_after_password_set_is_not_called ldaper = Ldaper.new assert ldaper.save end end -end \ No newline at end of file +end diff --git a/test/adapter_test.rb b/test/adapter_test.rb new file mode 100644 index 00000000..c2105bad --- /dev/null +++ b/test/adapter_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "test_helper" +require "authlogic/controller_adapters/rails_adapter" + +module Authlogic + module ControllerAdapters + class AbstractAdapterTest < ActiveSupport::TestCase + def test_controller + controller = Class.new(MockController) do + def controller.an_arbitrary_method + "bar" + end + end.new + adapter = Authlogic::ControllerAdapters::AbstractAdapter.new(controller) + + assert_equal controller, adapter.controller + assert controller.params.equal?(adapter.params) + assert adapter.respond_to?(:an_arbitrary_method) + assert_equal "bar", adapter.an_arbitrary_method + end + end + + class RailsAdapterTest < ActiveSupport::TestCase + def test_api_controller + controller = MockAPIController.new + adapter = Authlogic::ControllerAdapters::RailsAdapter.new(controller) + + assert_equal controller, adapter.controller + assert_nil adapter.cookies + end + end + + class RailsRequestAdapterTest < ActiveSupport::TestCase + def test_adapter_with_string_cookie + controller = MockController.new + controller.cookies["foo"] = "bar" + adapter = Authlogic::TestCase::RailsRequestAdapter.new(controller) + + assert adapter.cookies + end + + def test_adapter_with_hash_cookie + controller = MockController.new + controller.cookies["foo"] = { + value: "bar", + expires: nil + } + adapter = Authlogic::TestCase::RailsRequestAdapter.new(controller) + + assert adapter.cookies + end + end + end +end diff --git a/test/authenticates_many_test.rb b/test/authenticates_many_test.rb deleted file mode 100644 index 66e488b7..00000000 --- a/test/authenticates_many_test.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'test_helper' - -class AuthenticatesManyTest < ActiveSupport::TestCase - def test_scoping - zack = users(:zack) - ben = users(:ben) - binary_logic = companies(:binary_logic) - set_session_for(zack) - - assert !binary_logic.user_sessions.find - - set_session_for(ben) - - assert binary_logic.user_sessions.find - end -end \ No newline at end of file diff --git a/test/config_test.rb b/test/config_test.rb new file mode 100644 index 00000000..01b5cb1e --- /dev/null +++ b/test/config_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "test_helper" + +class ConfigTest < ActiveSupport::TestCase + def setup + @klass = Class.new { + extend Authlogic::Config + + def self.foobar(value = nil) + rw_config(:foobar_field, value, "default_foobar") + end + } + + @subklass = Class.new(@klass) + end + + def test_config + assert_equal({}, @klass.acts_as_authentic_config) + end + + def test_rw_config_read_with_default + assert "default_foobar", @klass.foobar + end + + def test_rw_config_write + assert_equal "my_foobar", @klass.foobar("my_foobar") + assert_equal "my_foobar", @klass.foobar + + assert_equal "my_new_foobar", @klass.foobar("my_new_foobar") + assert_equal "my_new_foobar", @klass.foobar + end + + def test_subclass_rw_config_write + assert_equal "subklass_foobar", @subklass.foobar("subklass_foobar") + assert_equal "default_foobar", @klass.foobar + end +end diff --git a/test/crypto_provider_test/aes256_test.rb b/test/crypto_provider_test/aes256_test.rb deleted file mode 100644 index d5c8d17d..00000000 --- a/test/crypto_provider_test/aes256_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'test_helper' - -module CryptoProviderTest - class AES256Test < ActiveSupport::TestCase - def test_encrypt - assert Authlogic::CryptoProviders::AES256.encrypt("mypass") - end - - def test_matches - hash = Authlogic::CryptoProviders::AES256.encrypt("mypass") - assert Authlogic::CryptoProviders::AES256.matches?(hash, "mypass") - end - end -end \ No newline at end of file diff --git a/test/crypto_provider_test/bcrypt_test.rb b/test/crypto_provider_test/bcrypt_test.rb index 5e44f269..8d9a462d 100644 --- a/test/crypto_provider_test/bcrypt_test.rb +++ b/test/crypto_provider_test/bcrypt_test.rb @@ -1,14 +1,16 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module CryptoProviderTest - class BCrpytTest < ActiveSupport::TestCase + class BCryptTest < ActiveSupport::TestCase def test_encrypt assert Authlogic::CryptoProviders::BCrypt.encrypt("mypass") end - + def test_matches hash = Authlogic::CryptoProviders::BCrypt.encrypt("mypass") assert Authlogic::CryptoProviders::BCrypt.matches?(hash, "mypass") end end -end \ No newline at end of file +end diff --git a/test/crypto_provider_test/md5/v2_test.rb b/test/crypto_provider_test/md5/v2_test.rb new file mode 100644 index 00000000..e9b34965 --- /dev/null +++ b/test/crypto_provider_test/md5/v2_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module MD5 + class V2Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::MD5::V2.stretches + end + + def teardown + Authlogic::CryptoProviders::MD5::V2.stretches = @default_stretches + end + + def test_encrypt + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" + + digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::MD5::V2.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "da62ac8b983606f684cea0b93a558283" + + digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" + + assert Authlogic::CryptoProviders::MD5::V2.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::MD5::V2.matches?(bad_digest, password, salt) + end + end + end +end diff --git a/test/crypto_provider_test/md5_test.rb b/test/crypto_provider_test/md5_test.rb new file mode 100644 index 00000000..fbf9ab70 --- /dev/null +++ b/test/crypto_provider_test/md5_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + class MD5Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::MD5.stretches + end + + def teardown + Authlogic::CryptoProviders::MD5.stretches = @default_stretches + end + + def test_encrypt + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" + + digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::MD5.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "9ac3a3a2e68f822f3482cbea3cbed9a3" + + digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" + + assert Authlogic::CryptoProviders::MD5.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::MD5.matches?(bad_digest, password, salt) + end + end +end diff --git a/test/crypto_provider_test/scrypt_test.rb b/test/crypto_provider_test/scrypt_test.rb new file mode 100644 index 00000000..d1b085d8 --- /dev/null +++ b/test/crypto_provider_test/scrypt_test.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + class SCryptTest < ActiveSupport::TestCase + def test_encrypt + assert Authlogic::CryptoProviders::SCrypt.encrypt("mypass") + end + + def test_matches + hash = Authlogic::CryptoProviders::SCrypt.encrypt("mypass") + assert Authlogic::CryptoProviders::SCrypt.matches?(hash, "mypass") + end + end +end diff --git a/test/crypto_provider_test/sha1/v2_test.rb b/test/crypto_provider_test/sha1/v2_test.rb new file mode 100644 index 00000000..0a0b5d76 --- /dev/null +++ b/test/crypto_provider_test/sha1/v2_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module VSHA1 + class V2Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha1::V2.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha1::V2.stretches = @default_stretches + end + + def test_encrypt + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "12d995b1f0af7d24d6f89d2e63dfbcb752384815" + + digest = Authlogic::CryptoProviders::Sha1::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha1::V2.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "af1e00f841ccc742c1e5879af35ca02b1978a1ac" + + digest = Authlogic::CryptoProviders::Sha1::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "12d995b1f0af7d24d6f89d2e63dfbcb752384815" + + assert Authlogic::CryptoProviders::Sha1::V2.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::Sha1::V2.matches?(bad_digest, password, salt) + end + end + end +end diff --git a/test/crypto_provider_test/sha1_test.rb b/test/crypto_provider_test/sha1_test.rb index 87653913..244259bd 100644 --- a/test/crypto_provider_test/sha1_test.rb +++ b/test/crypto_provider_test/sha1_test.rb @@ -1,23 +1,52 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module CryptoProviderTest class Sha1Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha1.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha1.stretches = @default_stretches + end + def test_encrypt - assert Authlogic::CryptoProviders::Sha1.encrypt("mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "5723d69f7ca1f8d63122c9cef4cf3c10d0482d3e" + + digest = Authlogic::CryptoProviders::Sha1.encrypt(password, salt) + + assert_equal digest, expected_digest end - + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha1.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "969f681d90a7d25679256e38cce3dc10db6d49c5" + + digest = Authlogic::CryptoProviders::Sha1.encrypt(password, salt) + + assert_equal digest, expected_digest + end + def test_matches - hash = Authlogic::CryptoProviders::Sha1.encrypt("mypass") - assert Authlogic::CryptoProviders::Sha1.matches?(hash, "mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "5723d69f7ca1f8d63122c9cef4cf3c10d0482d3e" + + assert Authlogic::CryptoProviders::Sha1.matches?(expected_digest, password, salt) end - - def test_old_restful_authentication_passwords + + def test_not_matches password = "test" salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" - digest = "00742970dc9e6319f8019fd54864d3ea740f04b1" - Authlogic::CryptoProviders::Sha1.stretches = 1 - assert Authlogic::CryptoProviders::Sha1.matches?(digest, nil, salt, password, nil) - Authlogic::CryptoProviders::Sha1.stretches = 10 + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::Sha1.matches?(bad_digest, password, salt) end end -end \ No newline at end of file +end diff --git a/test/crypto_provider_test/sha256/v2_test.rb b/test/crypto_provider_test/sha256/v2_test.rb new file mode 100644 index 00000000..1bfb2098 --- /dev/null +++ b/test/crypto_provider_test/sha256/v2_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module SHA256 + class V2Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha256::V2.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha256::V2.stretches = @default_stretches + end + + def test_encrypt + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "7f42a368b64a3c284c87b3ed3145b0c89f6bc49de931ca083e9c56a5c6b98e22" + + digest = Authlogic::CryptoProviders::Sha256::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha256::V2.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "1560ebc3b08d86828a7e9267379f7dbb847b6cc255135fc13210a4155a58b981" + + digest = Authlogic::CryptoProviders::Sha256::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "7f42a368b64a3c284c87b3ed3145b0c89f6bc49de931ca083e9c56a5c6b98e22" + + assert Authlogic::CryptoProviders::Sha256::V2.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::Sha256::V2.matches?(bad_digest, password, salt) + end + end + end +end diff --git a/test/crypto_provider_test/sha256_test.rb b/test/crypto_provider_test/sha256_test.rb index c4ee718d..983a1161 100644 --- a/test/crypto_provider_test/sha256_test.rb +++ b/test/crypto_provider_test/sha256_test.rb @@ -1,14 +1,52 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module CryptoProviderTest class Sha256Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha256.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha256.stretches = @default_stretches + end + def test_encrypt - assert Authlogic::CryptoProviders::Sha256.encrypt("mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3c4f802953726704088a3cd6d89237e9a279a8e8f43fa6de8549ca54b80b766c" + + digest = Authlogic::CryptoProviders::Sha256.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha256.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "06a2e9cd5552f2cdbc01ec61d52ce80d0bfba8f1bb689a356ac0193d42adc831" + + digest = Authlogic::CryptoProviders::Sha256.encrypt(password, salt) + + assert_equal digest, expected_digest end - + def test_matches - hash = Authlogic::CryptoProviders::Sha256.encrypt("mypass") - assert Authlogic::CryptoProviders::Sha256.matches?(hash, "mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "3c4f802953726704088a3cd6d89237e9a279a8e8f43fa6de8549ca54b80b766c" + + assert Authlogic::CryptoProviders::Sha256.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::Sha256.matches?(bad_digest, password, salt) end end end diff --git a/test/crypto_provider_test/sha512/v2_test.rb b/test/crypto_provider_test/sha512/v2_test.rb new file mode 100644 index 00000000..43da9f9b --- /dev/null +++ b/test/crypto_provider_test/sha512/v2_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module SHA512 + class V2Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha512::V2.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha512::V2.stretches = @default_stretches + end + + def test_encrypt + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "60e86eec0e7f858cc5cc6b42b31a847819b65e06317709ce27" \ + "79245d0776f18094dff9afbc66ae1e509f2b5e49f4d2ff3f632c8ee7c4683749f5fd028de5b085" + + digest = Authlogic::CryptoProviders::Sha512::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha512::V2.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "c4f546026f67a4fcce0e4df5905b845f75d9cfe1371eeaba99" \ + "a2c045940a7d08aa81837344752a9d4fb93883402114edd03955ed5962cd89b6e335c2ec5ca4a5" + digest = Authlogic::CryptoProviders::Sha512::V2.encrypt(password, salt) + + assert_equal digest, expected_digest + end + + def test_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "60e86eec0e7f858cc5cc6b42b31a847819b65e06317709ce27" \ + "79245d0776f18094dff9afbc66ae1e509f2b5e49f4d2ff3f632c8ee7c4683749f5fd028de5b085" + assert Authlogic::CryptoProviders::Sha512::V2.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + + assert !Authlogic::CryptoProviders::Sha512::V2.matches?(bad_digest, password, salt) + end + end + end +end diff --git a/test/crypto_provider_test/sha512_test.rb b/test/crypto_provider_test/sha512_test.rb index fcb369ba..b56068f9 100644 --- a/test/crypto_provider_test/sha512_test.rb +++ b/test/crypto_provider_test/sha512_test.rb @@ -1,14 +1,49 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module CryptoProviderTest class Sha512Test < ActiveSupport::TestCase + def setup + @default_stretches = Authlogic::CryptoProviders::Sha512.stretches + end + + def teardown + Authlogic::CryptoProviders::Sha512.stretches = @default_stretches + end + def test_encrypt - assert Authlogic::CryptoProviders::Sha512.encrypt("mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "9508ba2964d65501aa1d7798e8f250b35f50fadb870871f2bc1f" \ + "390872e8456e785633d06e17ffa4984a04cfa1a0e1ec29f15c31187b991e591393c6c0bffb61" + digest = Authlogic::CryptoProviders::Sha512.encrypt(password, salt) + assert_equal digest, expected_digest end - + + def test_encrypt_with_3_stretches + Authlogic::CryptoProviders::Sha512.stretches = 3 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "ed507752ef2e985a9e5661fedcbac8ad7536d4b80c87183c2027" \ + "3f568afb6f2112886fd786de00458eb2a14c640d9060c4688825e715cc1c3ecde8997d4ae556" + digest = Authlogic::CryptoProviders::Sha512.encrypt(password, salt) + assert_equal digest, expected_digest + end + def test_matches - hash = Authlogic::CryptoProviders::Sha512.encrypt("mypass") - assert Authlogic::CryptoProviders::Sha512.matches?(hash, "mypass") + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + expected_digest = "9508ba2964d65501aa1d7798e8f250b35f50fadb870871f2bc1f" \ + "390872e8456e785633d06e17ffa4984a04cfa1a0e1ec29f15c31187b991e591393c6c0bffb61" + assert Authlogic::CryptoProviders::Sha512.matches?(expected_digest, password, salt) + end + + def test_not_matches + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + bad_digest = "12345" + assert !Authlogic::CryptoProviders::Sha512.matches?(bad_digest, password, salt) end end -end \ No newline at end of file +end diff --git a/test/fixtures/admins.yml b/test/fixtures/admins.yml new file mode 100644 index 00000000..b1d96a25 --- /dev/null +++ b/test/fixtures/admins.yml @@ -0,0 +1,8 @@ +lumbergh: + login: lumbergh + crypted_password: <%= Authlogic::CryptoProviders::SCrypt.encrypt("lumberghrocks") %> + password_salt: <%= salt = Authlogic::Random.hex_token %> + persistence_token: e3d853f5aa0dacac5c257d03c4e097a3a7f51b182a8fc4f62096d05e939b019855aff0290157ac854e4195f13284ff5223f1996d0fd073e7e360171de54db278 + perishable_token: <%= Authlogic::Random.friendly_token %> + email: lumbergh@initech.com + role: TPS Supervisor diff --git a/test/fixtures/companies.yml b/test/fixtures/companies.yml index b62165bc..34b0b187 100644 --- a/test/fixtures/companies.yml +++ b/test/fixtures/companies.yml @@ -1,5 +1,5 @@ binary_logic: name: Binary Logic - + logic_over_data: - name: Logic Over Data \ No newline at end of file + name: Logic Over Data diff --git a/test/fixtures/employees.yml b/test/fixtures/employees.yml index c30e931c..93519cdb 100644 --- a/test/fixtures/employees.yml +++ b/test/fixtures/employees.yml @@ -6,7 +6,7 @@ drew: persistence_token: 5273d85ed156e9dbd6a7c1438d319ef8c8d41dd24368db6c222de11346c7b11e53ee08d45ecf619b1c1dc91233d22b372482b751b066d0a6f6f9bac42eacaabf first_name: Drew last_name: Gainor - + jennifer: company: logic_over_data email: jjohnson@logicoverdata.com diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 196444a9..f4797cfd 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -1,3 +1,8 @@ +# NB :ben and :zack use the legacy crypto provider (Sha512) ... when they're +# tested for valid_password?() it will transition their password +# (re: test/libs/user.rb). This could have unintended side-effects (like auto- +# resetting their persistence token when checking password) -- one solution +# is to just switch in users(:aaron) for those tests. ben: company: binary_logic projects: web_services @@ -10,7 +15,7 @@ ben: email: bjohnson@binarylogic.com first_name: Ben last_name: Johnson - + zack: company: logic_over_data projects: web_services @@ -21,4 +26,16 @@ zack: single_access_token: <%= Authlogic::Random.friendly_token %> email: zham@ziggityzack.com first_name: Zack - last_name: Ham \ No newline at end of file + last_name: Ham + +aaron: + company: cigital + projects: web_services + login: abedra + crypted_password: <%= Authlogic::CryptoProviders::SCrypt.encrypt("aaronrocks") %> + persistence_token: e3d853f5aa0dacac5c257d03c4e097a3a7f51b182a8fc4f62096d05e939b019855aff0290157ac854e4195f13284ff5223f1996d0fd073e7e360171de54db278 + single_access_token: <%= Authlogic::Random.friendly_token %> + perishable_token: <%= Authlogic::Random.friendly_token %> + email: abedra@cigital.com + first_name: Aaron + last_name: Bedra diff --git a/test/i18n/lol.yml b/test/i18n/lol.yml new file mode 100644 index 00000000..92ee4dba --- /dev/null +++ b/test/i18n/lol.yml @@ -0,0 +1,4 @@ +lol: + authlogic: + error_messages: + email_invalid: LOL email should be valid. diff --git a/test/i18n_test.rb b/test/i18n_test.rb index 0b8f093e..5548c815 100644 --- a/test/i18n_test.rb +++ b/test/i18n_test.rb @@ -1,33 +1,35 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" class I18nTest < ActiveSupport::TestCase def test_uses_authlogic_as_scope_by_default assert_equal :authlogic, Authlogic::I18n.scope end - + def test_can_set_scope - assert_nothing_raised { Authlogic::I18n.scope = [:a, :b] } - assert_equal [:a, :b], Authlogic::I18n.scope + assert_nothing_raised { Authlogic::I18n.scope = %i[a b] } + assert_equal %i[a b], Authlogic::I18n.scope Authlogic::I18n.scope = :authlogic end - + def test_uses_built_in_translator_by_default assert_equal Authlogic::I18n::Translator, Authlogic::I18n.translator.class end - + def test_can_set_custom_translator old_translator = Authlogic::I18n.translator - + assert_nothing_raised do Authlogic::I18n.translator = Class.new do - def translate(key, options = {}) + def translate(key, _options = {}) "Translated: #{key}" end end.new end assert_equal "Translated: x", Authlogic::I18n.translate(:x) - + Authlogic::I18n.translator = old_translator end end diff --git a/test/libs/admin.rb b/test/libs/admin.rb new file mode 100644 index 00000000..95c00c95 --- /dev/null +++ b/test/libs/admin.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# This model demonstrates an `after_save` callback. +class Admin < ActiveRecord::Base + acts_as_authentic do |c| + c.crypto_provider = Authlogic::CryptoProviders::SCrypt + end + + validates :password, confirmation: true + + after_save do + # In rails 5.1 `role_changed?` was deprecated in favor of `saved_change_to_role?`. + # + # > DEPRECATION WARNING: The behavior of `attribute_changed?` inside of + # > after callbacks will be changing in the next version of Rails. + # > The new return value will reflect the behavior of calling the method + # > after `save` returned (e.g. the opposite of what it returns now). To + # > maintain the current behavior, use `saved_change_to_attribute?` instead. + # + # So, in rails >= 5.2, we must use `saved_change_to_role?`. + if saved_change_to_role? + reset_password! + end + end +end diff --git a/test/libs/admin_session.rb b/test/libs/admin_session.rb new file mode 100644 index 00000000..9fe3eb2a --- /dev/null +++ b/test/libs/admin_session.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class AdminSession < Authlogic::Session::Base +end diff --git a/test/libs/affiliate.rb b/test/libs/affiliate.rb index 33aca637..e83d6c36 100644 --- a/test/libs/affiliate.rb +++ b/test/libs/affiliate.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class Affiliate < ActiveRecord::Base acts_as_authentic do |c| c.crypted_password_field = :pw_hash end - + belongs_to :company -end \ No newline at end of file +end diff --git a/test/libs/company.rb b/test/libs/company.rb index 588fea54..3d7fe835 100644 --- a/test/libs/company.rb +++ b/test/libs/company.rb @@ -1,6 +1,6 @@ +# frozen_string_literal: true + class Company < ActiveRecord::Base - authenticates_many :employee_sessions - authenticates_many :user_sessions - has_many :employees, :dependent => :destroy - has_many :users, :dependent => :destroy -end \ No newline at end of file + has_many :employees, dependent: :destroy + has_many :users, dependent: :destroy +end diff --git a/test/libs/employee.rb b/test/libs/employee.rb index 6703ebac..fb6276f7 100644 --- a/test/libs/employee.rb +++ b/test/libs/employee.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + class Employee < ActiveRecord::Base - acts_as_authentic do |c| - c.crypto_provider Authlogic::CryptoProviders::AES256 + acts_as_authentic do |config| + silence_warnings do + config.crypto_provider = Authlogic::CryptoProviders::Sha512 + end end - belongs_to :company -end \ No newline at end of file +end diff --git a/test/libs/employee_session.rb b/test/libs/employee_session.rb index ea30c39c..4106a1e4 100644 --- a/test/libs/employee_session.rb +++ b/test/libs/employee_session.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class EmployeeSession < Authlogic::Session::Base -end \ No newline at end of file +end diff --git a/test/libs/ldaper.rb b/test/libs/ldaper.rb index 97c8581a..6d26d672 100644 --- a/test/libs/ldaper.rb +++ b/test/libs/ldaper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ldaper < ActiveRecord::Base acts_as_authentic -end \ No newline at end of file +end diff --git a/test/libs/ordered_hash.rb b/test/libs/ordered_hash.rb deleted file mode 100644 index 7a869b6e..00000000 --- a/test/libs/ordered_hash.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Hash - def each(&block) - sorted_keys = keys.sort { |a, b| a.to_s <=> b.to_s } - sorted_keys.each do |key| - yield key, self[key] - end - self - end -end \ No newline at end of file diff --git a/test/libs/project.rb b/test/libs/project.rb index 72f65d08..ea737ab8 100644 --- a/test/libs/project.rb +++ b/test/libs/project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Project < ActiveRecord::Base has_and_belongs_to_many :users -end \ No newline at end of file +end diff --git a/test/libs/user.rb b/test/libs/user.rb index 3e5a66da..9f574857 100644 --- a/test/libs/user.rb +++ b/test/libs/user.rb @@ -1,5 +1,77 @@ +# frozen_string_literal: true + class User < ActiveRecord::Base - acts_as_authentic + EMAIL = / + \A + [A-Z0-9_.&%+\-']+ # mailbox + @ + (?:[A-Z0-9\-]+\.)+ # subdomains + (?:[A-Z]{2,25}) # TLD + \z + /ix.freeze + LOGIN = /\A[a-zA-Z0-9_][a-zA-Z0-9\.+\-_@ ]+\z/.freeze + + acts_as_authentic do |c| + c.crypto_provider = Authlogic::CryptoProviders::SCrypt + c.transition_from_crypto_providers Authlogic::CryptoProviders::Sha512 + end belongs_to :company has_and_belongs_to_many :projects -end \ No newline at end of file + + # Validations + # ----------- + # + # In Authlogic 4.4.0, we deprecated the features of Authlogic related to + # validating email, login, and password. In 5.0.0 these features were dropped. + # People will instead use normal ActiveRecord validations. + # + # The following validations represent what Authlogic < 5 used as defaults. + validates :email, + format: { + with: EMAIL, + message: proc { + ::Authlogic::I18n.t( + "error_messages.email_invalid", + default: "should look like an email address." + ) + } + }, + length: { maximum: 100 }, + uniqueness: { + case_sensitive: false, + if: :will_save_change_to_email? + } + + validates :login, + format: { + with: LOGIN, + message: proc { + ::Authlogic::I18n.t( + "error_messages.login_invalid", + default: "should use only letters, numbers, spaces, and .-_@+ please." + ) + } + }, + length: { within: 3..100 }, + uniqueness: { + # Our User model will test `case_sensitive: true`. Other models, like + # Employee and Admin do not validate uniqueness, and thus, for them, + # `find_by_smart_case_login_field` will be case-insensitive. See eg. + # `test_find_by_smart_case_login_field` in + # `test/acts_as_authentic_test/login_test.rb` + case_sensitive: true, + if: :will_save_change_to_login? + } + + validates :password, + confirmation: { if: :require_password? }, + length: { + minimum: 8, + if: :require_password? + } + validates :password_confirmation, + length: { + minimum: 8, + if: :require_password? + } +end diff --git a/test/libs/user_session.rb b/test/libs/user_session.rb index b227d860..287cd487 100644 --- a/test/libs/user_session.rb +++ b/test/libs/user_session.rb @@ -1,6 +1,27 @@ +# frozen_string_literal: true + class UserSession < Authlogic::Session::Base end class BackOfficeUserSession < Authlogic::Session::Base +end + +class WackyUserSession < Authlogic::Session::Base + attr_accessor :counter authenticate_with User + + def initialize + @counter = 0 + super + end + + def persist_by_false + self.counter += 1 + false + end + + def persist_by_true + self.counter += 1 + true + end end diff --git a/test/random_test.rb b/test/random_test.rb index dab4cd51..905a7a05 100644 --- a/test/random_test.rb +++ b/test/random_test.rb @@ -1,42 +1,15 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" class RandomTest < ActiveSupport::TestCase - def test_random_tokens_are_indeed_random - # this might fail if you are *really* unlucky :) - with_any_random do - assert_not_equal Authlogic::Random.hex_token, Authlogic::Random.hex_token - assert_not_equal Authlogic::Random.friendly_token, Authlogic::Random.friendly_token - end + def test_that_hex_tokens_are_unique + tokens = Array.new(100) { Authlogic::Random.hex_token } + assert_equal tokens.size, tokens.uniq.size end - private - def with_any_random(&block) - [true, false].each {|val| with_secure_random_enabled(val, &block)} - end - - def with_secure_random_enabled(enabled = true) - # can't really test SecureRandom if we don't have an implementation - return if enabled && !Authlogic::Random::SecureRandom - - current_sec_rand = Authlogic::Random::SecureRandom - reload_authlogic_with_sec_random!(current_sec_rand, enabled) - - yield - ensure - reload_authlogic_with_sec_random!(current_sec_rand) - end - - def reload_authlogic_with_sec_random!(secure_random, enabled = true) - silence_warnings do - secure_random.parent.const_set(secure_random.name.sub("#{secure_random.parent}::", ''), enabled ? secure_random : nil) - load(File.dirname(__FILE__) + '/../lib/authlogic/random.rb') - end - end - - def silence_warnings - old_verbose, $VERBOSE = $VERBOSE, nil - yield - ensure - $VERBOSE = old_verbose - end -end \ No newline at end of file + def test_that_friendly_tokens_are_unique + tokens = Array.new(100) { Authlogic::Random.friendly_token } + assert_equal tokens.size, tokens.uniq.size + end +end diff --git a/test/session_test/activation_test.rb b/test/session_test/activation_test.rb index d2c7f0bd..2ead68b2 100644 --- a/test/session_test/activation_test.rb +++ b/test/session_test/activation_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module ActivationTest @@ -6,9 +8,9 @@ class ClassMethodsTest < ActiveSupport::TestCase def test_activated assert UserSession.activated? Authlogic::Session::Base.controller = nil - assert !UserSession.activated? + refute UserSession.activated? end - + def test_controller Authlogic::Session::Base.controller = nil assert_nil Authlogic::Session::Base.controller @@ -20,18 +22,18 @@ def test_controller thread1.join assert_nil Authlogic::Session::Base.controller - + thread2 = Thread.new do controller = MockController.new Authlogic::Session::Base.controller = controller assert_equal controller, Authlogic::Session::Base.controller end thread2.join - + assert_nil Authlogic::Session::Base.controller end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_init UserSession.controller = nil @@ -40,4 +42,4 @@ def test_init end end end -end \ No newline at end of file +end diff --git a/test/session_test/active_record_trickery_test.rb b/test/session_test/active_record_trickery_test.rb index bc766651..6eebbf8e 100644 --- a/test/session_test/active_record_trickery_test.rb +++ b/test/session_test/active_record_trickery_test.rb @@ -1,32 +1,74 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module ActiveRecordTrickeryTest class ClassMethodsTest < ActiveSupport::TestCase + # If test_human_name is executed after test_i18n_of_human_name the test will fail. + i_suck_and_my_tests_are_order_dependent! + def test_human_attribute_name assert_equal "Some attribute", UserSession.human_attribute_name("some_attribute") assert_equal "Some attribute", UserSession.human_attribute_name(:some_attribute) end - + def test_human_name assert_equal "Usersession", UserSession.human_name end - - def test_self_and_descendents_from_active_record - assert_equal [UserSession], UserSession.self_and_descendents_from_active_record + + def test_i18n_of_human_name + I18n.backend.store_translations "en", authlogic: { models: { user_session: "MySession" } } + assert_equal "MySession", UserSession.human_name + end + + def test_i18n_of_model_name_human + I18n.backend.store_translations "en", authlogic: { models: { user_session: "MySession" } } + assert_equal "MySession", UserSession.model_name.human end - - def test_self_and_descendants_from_active_record - assert_equal [UserSession], UserSession.self_and_descendants_from_active_record + + def test_model_name + assert_equal "UserSession", UserSession.model_name.name + assert_equal "user_session", UserSession.model_name.singular + assert_equal "user_sessions", UserSession.model_name.plural end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_new_record session = UserSession.new assert session.new_record? end - + + def test_to_key + ben = users(:ben) + session = UserSession.new(ben) + assert_nil session.to_key + + session.save + assert_not_nil session.to_key + assert_equal ben.to_key, session.to_key + end + + def test_persisted + session = UserSession.new(users(:ben)) + refute session.persisted? + + session.save + assert session.persisted? + + session.destroy + refute session.persisted? + end + + def test_destroyed? + session = UserSession.create(users(:ben)) + refute session.destroyed? + + session.destroy + assert session.destroyed? + end + def test_to_model session = UserSession.new assert_equal session, session.to_model diff --git a/test/session_test/brute_force_protection_test.rb b/test/session_test/brute_force_protection_test.rb index 354b0b69..e5f8f704 100644 --- a/test/session_test/brute_force_protection_test.rb +++ b/test/session_test/brute_force_protection_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module BruteForceProtectionTest @@ -6,74 +8,80 @@ class ConfigTest < ActiveSupport::TestCase def test_consecutive_failed_logins_limit UserSession.consecutive_failed_logins_limit = 10 assert_equal 10, UserSession.consecutive_failed_logins_limit - + UserSession.consecutive_failed_logins_limit 50 assert_equal 50, UserSession.consecutive_failed_logins_limit end - + def test_failed_login_ban_for UserSession.failed_login_ban_for = 10 assert_equal 10, UserSession.failed_login_ban_for - + UserSession.failed_login_ban_for 2.hours assert_equal 2.hours.to_i, UserSession.failed_login_ban_for end end - - class InstaceMethodsTest < ActiveSupport::TestCase + + class InstanceMethodsTest < ActiveSupport::TestCase def test_under_limit ben = users(:ben) ben.failed_login_count = UserSession.consecutive_failed_logins_limit - 1 assert ben.save - assert UserSession.create(:login => ben.login, :password => "benrocks") + session = UserSession.create(login: ben.login, password: "benrocks") + refute session.new_session? end def test_exceeded_limit ben = users(:ben) ben.failed_login_count = UserSession.consecutive_failed_logins_limit assert ben.save - assert UserSession.create(:login => ben.login, :password => "benrocks").new_session? + session = UserSession.create(login: ben.login, password: "benrocks") + assert session.new_session? assert UserSession.create(ben).new_session? + ben.reload ben.updated_at = (UserSession.failed_login_ban_for + 2.hours.to_i).seconds.ago - assert !UserSession.create(ben).new_session? + refute UserSession.create(ben).new_session? end - + def test_exceeding_failed_logins_limit UserSession.consecutive_failed_logins_limit = 2 ben = users(:ben) - + 2.times do |i| - session = UserSession.new(:login => ben.login, :password => "badpassword1") - assert !session.save - assert session.errors[:password].size > 0 + session = UserSession.new(login: ben.login, password: "badpassword1") + refute session.save + refute session.errors[:password].empty? assert_equal i + 1, ben.reload.failed_login_count end - - session = UserSession.new(:login => ben.login, :password => "badpassword2") - assert !session.save - assert session.errors[:password].size == 0 + + session = UserSession.new(login: ben.login, password: "badpassword2") + refute session.save + assert session.errors[:password].empty? assert_equal 3, ben.reload.failed_login_count - + UserSession.consecutive_failed_logins_limit = 50 end - + def test_exceeded_ban_for UserSession.consecutive_failed_logins_limit = 2 UserSession.generalize_credentials_error_messages true ben = users(:ben) - + 2.times do |i| - session = UserSession.new(:login => ben.login, :password => "badpassword1") - assert !session.save + session = UserSession.new(login: ben.login, password: "badpassword1") + refute session.save assert session.invalid_password? assert_equal i + 1, ben.reload.failed_login_count end - - ActiveRecord::Base.connection.execute("update users set updated_at = '#{1.day.ago.to_s(:db)}' where login = '#{ben.login}'") - session = UserSession.new(:login => ben.login, :password => "benrocks") + + ActiveRecord::Base.connection.execute( + "update users set updated_at = '#{1.day.ago.to_formatted_s(:db)}' + where login = '#{ben.login}'" + ) + session = UserSession.new(login: ben.login, password: "benrocks") assert session.save assert_equal 0, ben.reload.failed_login_count - + UserSession.consecutive_failed_logins_limit = 50 UserSession.generalize_credentials_error_messages false end @@ -81,21 +89,24 @@ def test_exceeded_ban_for def test_exceeded_ban_and_failed_doesnt_ban_again UserSession.consecutive_failed_logins_limit = 2 ben = users(:ben) - + 2.times do |i| - session = UserSession.new(:login => ben.login, :password => "badpassword1") - assert !session.save - assert session.errors[:password].size > 0 + session = UserSession.new(login: ben.login, password: "badpassword1") + refute session.save + refute session.errors[:password].empty? assert_equal i + 1, ben.reload.failed_login_count end - - ActiveRecord::Base.connection.execute("update users set updated_at = '#{1.day.ago.to_s(:db)}' where login = '#{ben.login}'") - session = UserSession.new(:login => ben.login, :password => "badpassword1") - assert !session.save + + ActiveRecord::Base.connection.execute( + "update users set updated_at = '#{1.day.ago.to_formatted_s(:db)}' + where login = '#{ben.login}'" + ) + session = UserSession.new(login: ben.login, password: "badpassword1") + refute session.save assert_equal 1, ben.reload.failed_login_count - + UserSession.consecutive_failed_logins_limit = 50 end end end -end \ No newline at end of file +end diff --git a/test/session_test/callbacks_test.rb b/test/session_test/callbacks_test.rb index 4fee22ae..58198237 100644 --- a/test/session_test/callbacks_test.rb +++ b/test/session_test/callbacks_test.rb @@ -1,6 +1,42 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class CallbacksTest < ActiveSupport::TestCase + def setup + WackyUserSession.reset_callbacks(:persist) + end + + def test_no_callbacks + assert_equal [], WackyUserSession._persist_callbacks.map(&:filter) + session = WackyUserSession.new + session.send(:run_callbacks, :persist) + assert_equal 0, session.counter + end + + def test_true_callback_cancelling_later_callbacks + WackyUserSession.persist :persist_by_true, :persist_by_false + assert_equal( + %i[persist_by_true persist_by_false], + WackyUserSession._persist_callbacks.map(&:filter) + ) + + session = WackyUserSession.new + session.send(:run_callbacks, :persist) + assert_equal 1, session.counter + end + + def test_false_callback_continuing_to_later_callbacks + WackyUserSession.persist :persist_by_false, :persist_by_true + assert_equal( + %i[persist_by_false persist_by_true], + WackyUserSession._persist_callbacks.map(&:filter) + ) + + session = WackyUserSession.new + session.send(:run_callbacks, :persist) + assert_equal 2, session.counter + end end -end \ No newline at end of file +end diff --git a/test/session_test/cookies_test.rb b/test/session_test/cookies_test.rb index f24788a1..2177f1f3 100644 --- a/test/session_test/cookies_test.rb +++ b/test/session_test/cookies_test.rb @@ -1,112 +1,272 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module CookiesTest - class ConfiTest < ActiveSupport::TestCase + class ConfigTest < ActiveSupport::TestCase def test_cookie_key UserSession.cookie_key = "my_cookie_key" assert_equal "my_cookie_key", UserSession.cookie_key - + UserSession.cookie_key "user_credentials" assert_equal "user_credentials", UserSession.cookie_key end - + def test_default_cookie_key assert_equal "user_credentials", UserSession.cookie_key assert_equal "back_office_user_credentials", BackOfficeUserSession.cookie_key end - + def test_remember_me UserSession.remember_me = true assert_equal true, UserSession.remember_me session = UserSession.new assert_equal true, session.remember_me - + UserSession.remember_me false assert_equal false, UserSession.remember_me session = UserSession.new assert_equal false, session.remember_me end - + def test_remember_me_for UserSession.remember_me_for = 3.years assert_equal 3.years, UserSession.remember_me_for session = UserSession.new session.remember_me = true assert_equal 3.years, session.remember_me_for - + UserSession.remember_me_for 3.months assert_equal 3.months, UserSession.remember_me_for session = UserSession.new session.remember_me = true assert_equal 3.months, session.remember_me_for end + + def test_secure + assert_equal true, UserSession.secure + session = UserSession.new + assert_equal true, session.secure + + UserSession.secure false + assert_equal false, UserSession.secure + session = UserSession.new + assert_equal false, session.secure + end + + def test_httponly + assert_equal true, UserSession.httponly + session = UserSession.new + assert_equal true, session.httponly + + UserSession.httponly false + assert_equal false, UserSession.httponly + session = UserSession.new + assert_equal false, session.httponly + end + + def test_same_site + assert_nil UserSession.same_site + assert_nil UserSession.new.same_site + + UserSession.same_site "Strict" + assert_equal "Strict", UserSession.same_site + session = UserSession.new + assert_equal "Strict", session.same_site + session.same_site = "Lax" + assert_equal "Lax", session.same_site + session.same_site = "None" + assert_equal "None", session.same_site + + assert_raise(ArgumentError) { UserSession.same_site "foo" } + assert_raise(ArgumentError) { UserSession.new.same_site "foo" } + end + + def test_sign_cookie + UserSession.sign_cookie = true + assert_equal true, UserSession.sign_cookie + session = UserSession.new + assert_equal true, session.sign_cookie + + UserSession.sign_cookie false + assert_equal false, UserSession.sign_cookie + session = UserSession.new + assert_equal false, session.sign_cookie + end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_credentials session = UserSession.new - session.credentials = {:remember_me => true} + session.credentials = { remember_me: true } + assert_equal({}, session.credentials) + end + + def test_credentials_assignment + session = UserSession.new + session.credentials = { remember_me: true } assert_equal true, session.remember_me end - + def test_remember_me session = UserSession.new assert_equal false, session.remember_me - assert !session.remember_me? - + refute session.remember_me? + session.remember_me = false assert_equal false, session.remember_me - assert !session.remember_me? - + refute session.remember_me? + session.remember_me = true assert_equal true, session.remember_me assert session.remember_me? - + session.remember_me = nil assert_nil session.remember_me - assert !session.remember_me? - + refute session.remember_me? + session.remember_me = "1" assert_equal "1", session.remember_me assert session.remember_me? - + session.remember_me = "true" assert_equal "true", session.remember_me assert session.remember_me? end - + def test_remember_me_until session = UserSession.new assert_nil session.remember_me_until - + session.remember_me = true assert 3.months.from_now <= session.remember_me_until end - + def test_persist_persist_by_cookie ben = users(:ben) - assert !UserSession.find + refute UserSession.find set_cookie_for(ben) assert session = UserSession.find assert_equal ben, session.record end - + + def test_persist_persist_by_cookie_with_blank_persistence_token + ben = users(:ben) + ben.update_column(:persistence_token, "") + refute UserSession.find + set_cookie_for(ben) + refute UserSession.find + end + + def test_remember_me_expired + ben = users(:ben) + session = UserSession.new(ben) + session.remember_me = true + assert session.save + refute session.remember_me_expired? + + session = UserSession.new(ben) + session.remember_me = false + assert session.save + refute session.remember_me_expired? + end + def test_after_save_save_cookie ben = users(:ben) session = UserSession.new(ben) assert session.save - assert_equal "#{ben.persistence_token}::#{ben.id}", controller.cookies["user_credentials"] + assert_equal( + "#{ben.persistence_token}::#{ben.id}", + controller.cookies["user_credentials"] + ) + end + + def test_after_save_save_cookie_encrypted + ben = users(:ben) + + assert_nil controller.cookies["user_credentials"] + payload = "#{ben.persistence_token}::#{ben.id}" + + session = UserSession.new(ben) + session.encrypt_cookie = true + assert session.save + assert_equal payload, controller.cookies.encrypted["user_credentials"] + assert_equal( + Authlogic::TestCase::MockEncryptedCookieJar.encrypt(payload), + controller.cookies.encrypted.parent_jar["user_credentials"] + ) end - + + def test_after_save_save_cookie_signed + ben = users(:ben) + + assert_nil controller.cookies["user_credentials"] + payload = "#{ben.persistence_token}::#{ben.id}" + + session = UserSession.new(ben) + session.sign_cookie = true + assert session.save + assert_equal payload, controller.cookies.signed["user_credentials"] + assert_equal( + "#{payload}--#{Digest::SHA1.hexdigest payload}", + controller.cookies.signed.parent_jar["user_credentials"] + ) + end + + def test_after_save_save_cookie_with_remember_me + Timecop.freeze do + ben = users(:ben) + session = UserSession.new(ben) + session.remember_me = true + assert session.save + assert_equal( + "#{ben.persistence_token}::#{ben.id}::#{session.remember_me_until.iso8601}", + controller.cookies["user_credentials"] + ) + end + end + + def test_after_save_save_cookie_with_same_site + session = UserSession.new(users(:ben)) + session.same_site = "Strict" + assert session.save + assert_equal( + "Strict", + controller.cookies.set_cookies["user_credentials"][:same_site] + ) + end + def test_after_destroy_destroy_cookie ben = users(:ben) set_cookie_for(ben) session = UserSession.find assert controller.cookies["user_credentials"] assert session.destroy - assert !controller.cookies["user_credentials"] + refute controller.cookies["user_credentials"] + end + + def test_string_cookie + payload = "bar" + controller.cookies["foo"] = payload + assert_equal(payload, controller.cookies["foo"]) + end + + def test_string_cookie_signed + payload = "bar" + session = UserSession.new + session.sign_cookie = true + controller.cookies["foo"] = payload + assert_equal(payload, controller.cookies.signed["foo"]) + end + + def test_string_cookie_encrypted + payload = "bar" + session = UserSession.new + session.encrypt_cookie = true + controller.cookies["foo"] = payload + assert_equal(payload, controller.cookies.encrypted["foo"]) end end end -end \ No newline at end of file +end diff --git a/test/session_test/existence_test.rb b/test/session_test/existence_test.rb index 701599b0..52005d9c 100644 --- a/test/session_test/existence_test.rb +++ b/test/session_test/existence_test.rb @@ -1,64 +1,88 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module ExistenceTest class ClassMethodsTest < ActiveSupport::TestCase - def test_create + def test_create_with_good_credentials ben = users(:ben) - assert UserSession.create(:login => "somelogin", :password => "badpw2").new_session? - assert !UserSession.create(:login => ben.login, :password => "benrocks").new_session? - assert_raise(Authlogic::Session::Existence::SessionInvalidError) { UserSession.create!(:login => ben.login, :password => "badpw") } - assert !UserSession.create!(:login => ben.login, :password => "benrocks").new_session? + session = UserSession.create(login: ben.login, password: "benrocks") + refute session.new_session? + end + + def test_create_with_bad_credentials + session = UserSession.create(login: "somelogin", password: "badpw2") + assert session.new_session? + end + + def test_create_bang + ben = users(:ben) + err = assert_raise(Authlogic::Session::Existence::SessionInvalidError) do + UserSession.create!(login: ben.login, password: "badpw") + end + assert_includes err.message, "Password is not valid" + refute UserSession.create!(login: ben.login, password: "benrocks").new_session? end end - - class IsntaceMethodsTest < ActiveSupport::TestCase + + class InstanceMethodsTest < ActiveSupport::TestCase def test_new_session session = UserSession.new assert session.new_session? - + set_session_for(users(:ben)) session = UserSession.find - assert !session.new_session? + refute session.new_session? end - + def test_save_with_nothing session = UserSession.new - assert !session.save + refute session.save assert session.new_session? end - + def test_save_with_block - ben = users(:ben) session = UserSession.new block_result = session.save do |result| - assert !result + refute result end - assert !block_result + refute block_result assert session.new_session? end - + def test_save_with_bang session = UserSession.new assert_raise(Authlogic::Session::Existence::SessionInvalidError) { session.save! } - + session.unauthorized_record = users(:ben) assert_nothing_raised { session.save! } end - + def test_destroy ben = users(:ben) session = UserSession.new - assert !session.valid? - assert !session.errors.empty? + refute session.valid? + refute session.errors.empty? assert session.destroy assert session.errors.empty? session.unauthorized_record = ben assert session.save assert session.record assert session.destroy - assert !session.record + refute session.record + end + end + + class SessionInvalidErrorTest < ActiveSupport::TestCase + def test_message + session = UserSession.new + assert !session.valid? + error = Authlogic::Session::Existence::SessionInvalidError.new(session) + message = "Your session is invalid and has the following errors: " + + session.errors.full_messages.to_sentence + assert_equal message, error.message end end end -end \ No newline at end of file +end diff --git a/test/session_test/foundation_test.rb b/test/session_test/foundation_test.rb new file mode 100644 index 00000000..4ffa5703 --- /dev/null +++ b/test/session_test/foundation_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "test_helper" + +# We forbid the use of AC::Parameters, and we have a test to that effect, but we +# do not want a development dependency on `actionpack`, so we define it here. +module ActionController + class Parameters; end +end + +module SessionTest + class FoundationTest < ActiveSupport::TestCase + def test_credentials_raise_if_not_a_hash + session = UserSession.new + e = assert_raises(TypeError) { + session.credentials = ActionController::Parameters.new + } + assert_equal( + ::Authlogic::Session::Base::E_AC_PARAMETERS, + e.message + ) + end + end +end diff --git a/test/session_test/http_auth_test.rb b/test/session_test/http_auth_test.rb index 3e89fbf5..750ef913 100644 --- a/test/session_test/http_auth_test.rb +++ b/test/session_test/http_auth_test.rb @@ -1,28 +1,60 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class HttpAuthTest < ActiveSupport::TestCase - class ConfiTest < ActiveSupport::TestCase + class ConfigTest < ActiveSupport::TestCase def test_allow_http_basic_auth UserSession.allow_http_basic_auth = false assert_equal false, UserSession.allow_http_basic_auth - + UserSession.allow_http_basic_auth true assert_equal true, UserSession.allow_http_basic_auth end + + def test_request_http_basic_auth + UserSession.request_http_basic_auth = true + assert_equal true, UserSession.request_http_basic_auth + + UserSession.request_http_basic_auth = false + assert_equal false, UserSession.request_http_basic_auth + end + + def test_http_basic_auth_realm + assert_equal "Application", UserSession.http_basic_auth_realm + UserSession.http_basic_auth_realm = "TestRealm" + assert_equal "TestRealm", UserSession.http_basic_auth_realm + end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_persist_persist_by_http_auth - ben = users(:ben) - http_basic_auth_for { assert !UserSession.find } - http_basic_auth_for(ben) do + UserSession.allow_http_basic_auth = true + + aaron = users(:aaron) + http_basic_auth_for do + refute UserSession.find + end + http_basic_auth_for(aaron) do + assert session = UserSession.find + assert_equal aaron, session.record + assert_equal aaron.login, session.login + assert_equal "aaronrocks", session.send(:protected_password) + refute controller.http_auth_requested? + end + unset_session + UserSession.request_http_basic_auth = true + UserSession.http_basic_auth_realm = "PersistTestRealm" + http_basic_auth_for(aaron) do assert session = UserSession.find - assert_equal ben, session.record - assert_equal ben.login, session.login - assert_equal "benrocks", session.send(:protected_password) + assert_equal aaron, session.record + assert_equal aaron.login, session.login + assert_equal "aaronrocks", session.send(:protected_password) + assert_equal "PersistTestRealm", controller.realm + assert controller.http_auth_requested? end end end end -end \ No newline at end of file +end diff --git a/test/session_test/id_test.rb b/test/session_test/id_test.rb index 653aa7ce..e68e5ba0 100644 --- a/test/session_test/id_test.rb +++ b/test/session_test/id_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class IdTest < ActiveSupport::TestCase @@ -7,11 +9,11 @@ def test_credentials session.credentials = [:my_id] assert_equal :my_id, session.id end - + def test_id session = UserSession.new session.id = :my_id assert_equal :my_id, session.id end end -end \ No newline at end of file +end diff --git a/test/session_test/klass_test.rb b/test/session_test/klass_test.rb index 5d4b7f3a..8aa7ca15 100644 --- a/test/session_test/klass_test.rb +++ b/test/session_test/klass_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module KlassTest @@ -7,12 +9,12 @@ def test_authenticate_with UserSession.authenticate_with = Employee assert_equal "Employee", UserSession.klass_name assert_equal Employee, UserSession.klass - + UserSession.authenticate_with User assert_equal "User", UserSession.klass_name assert_equal User, UserSession.klass end - + def test_klass assert_equal User, UserSession.klass end @@ -20,13 +22,13 @@ def test_klass def test_klass_name assert_equal "User", UserSession.klass_name end - - def test_guessed_klass_name - assert_equal "User", UserSession.guessed_klass_name - assert_equal "BackOfficeUser", BackOfficeUserSession.guessed_klass_name + + def test_klass_name_uses_custom_name + assert_equal "User", UserSession.klass_name + assert_equal "BackOfficeUser", BackOfficeUserSession.klass_name end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_record_method ben = users(:ben) @@ -37,4 +39,4 @@ def test_record_method end end end -end \ No newline at end of file +end diff --git a/test/session_test/magic_columns_test.rb b/test/session_test/magic_columns_test.rb index c5f5d8ac..6dafd11e 100644 --- a/test/session_test/magic_columns_test.rb +++ b/test/session_test/magic_columns_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module MagicColumnsTest @@ -6,57 +8,55 @@ class ConfigTest < ActiveSupport::TestCase def test_last_request_at_threshold_config UserSession.last_request_at_threshold = 2.minutes assert_equal 2.minutes, UserSession.last_request_at_threshold - + UserSession.last_request_at_threshold 0 assert_equal 0, UserSession.last_request_at_threshold end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_after_persisting_set_last_request_at ben = users(:ben) - assert !UserSession.create(ben).new_session? - + refute UserSession.create(ben).new_session? + set_cookie_for(ben) old_last_request_at = ben.last_request_at assert UserSession.find ben.reload - assert ben.last_request_at != old_last_request_at + assert ben.last_request_at > old_last_request_at end - + def test_valid_increase_failed_login_count ben = users(:ben) old_failed_login_count = ben.failed_login_count - assert UserSession.create(:login => ben.login, :password => "wrong").new_session? + session = UserSession.create(login: ben.login, password: "wrong") + assert session.new_session? ben.reload assert_equal old_failed_login_count + 1, ben.failed_login_count end - + def test_before_save_update_info - ben = users(:ben) - + aaron = users(:aaron) + # increase failed login count - assert UserSession.create(:login => ben.login, :password => "wrong").new_session? - ben.reload - - # grab old values - old_login_count = ben.login_count - old_failed_login_count = ben.failed_login_count - old_last_login_at = ben.last_login_at - old_current_login_at = ben.current_login_at - old_last_login_ip = ben.last_login_ip - old_current_login_ip = ben.current_login_ip - - assert !UserSession.create(:login => ben.login, :password => "benrocks").new_session? - - ben.reload - assert_equal old_login_count + 1, ben.login_count - assert_equal 0, ben.failed_login_count - assert_equal old_current_login_at, ben.last_login_at - assert ben.current_login_at != old_current_login_at - assert_equal old_current_login_ip, ben.last_login_ip - assert_equal "1.1.1.1", ben.current_login_ip + session = UserSession.create(login: aaron.login, password: "wrong") + assert session.new_session? + aaron.reload + assert_equal 0, aaron.login_count + assert_nil aaron.current_login_at + assert_nil aaron.current_login_ip + + session = UserSession.create(login: aaron.login, password: "aaronrocks") + assert session.valid? + + aaron.reload + assert_equal 1, aaron.login_count + assert_equal 0, aaron.failed_login_count + assert_nil aaron.last_login_at + assert_not_nil aaron.current_login_at + assert_nil aaron.last_login_ip + assert_equal "1.1.1.1", aaron.current_login_ip end end end -end \ No newline at end of file +end diff --git a/test/session_test/magic_states_test.rb b/test/session_test/magic_states_test.rb index 0ce9ec1d..1890be26 100644 --- a/test/session_test/magic_states_test.rb +++ b/test/session_test/magic_states_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module SessionTest @@ -6,55 +8,53 @@ class ConfigTest < ActiveSupport::TestCase def test_disable_magic_states_config UserSession.disable_magic_states = true assert_equal true, UserSession.disable_magic_states - + UserSession.disable_magic_states false assert_equal false, UserSession.disable_magic_states end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_disabling_magic_states UserSession.disable_magic_states = true - ben = users(:ben) ben.update_attribute(:active, false) - assert UserSession.create(ben) - + refute UserSession.create(ben).new_session? UserSession.disable_magic_states = false end - + def test_validate_validate_magic_states_active session = UserSession.new ben = users(:ben) session.unauthorized_record = ben assert session.valid? - + ben.update_attribute(:active, false) - assert !session.valid? - assert session.errors[:base].size > 0 + refute session.valid? + refute session.errors[:base].empty? end - + def test_validate_validate_magic_states_approved session = UserSession.new ben = users(:ben) session.unauthorized_record = ben assert session.valid? - + ben.update_attribute(:approved, false) - assert !session.valid? - assert session.errors[:base].size > 0 + refute session.valid? + refute session.errors[:base].empty? end - + def test_validate_validate_magic_states_confirmed session = UserSession.new ben = users(:ben) session.unauthorized_record = ben assert session.valid? - + ben.update_attribute(:confirmed, false) - assert !session.valid? - assert session.errors[:base].size > 0 + refute session.valid? + refute session.errors[:base].empty? end end end -end \ No newline at end of file +end diff --git a/test/session_test/params_test.rb b/test/session_test/params_test.rb index 7c5b23c4..5d280f13 100644 --- a/test/session_test/params_test.rb +++ b/test/session_test/params_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module ParamsTest @@ -6,43 +8,49 @@ class ConfigTest < ActiveSupport::TestCase def test_params_key UserSession.params_key = "my_params_key" assert_equal "my_params_key", UserSession.params_key - + UserSession.params_key "user_credentials" assert_equal "user_credentials", UserSession.params_key end - + def test_single_access_allowed_request_types UserSession.single_access_allowed_request_types = ["my request type"] assert_equal ["my request type"], UserSession.single_access_allowed_request_types - - UserSession.single_access_allowed_request_types ["application/rss+xml", "application/atom+xml"] - assert_equal ["application/rss+xml", "application/atom+xml"], UserSession.single_access_allowed_request_types + UserSession.single_access_allowed_request_types( + ["application/rss+xml", "application/atom+xml"] + ) + assert_equal( + ["application/rss+xml", "application/atom+xml"], + UserSession.single_access_allowed_request_types + ) end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_persist_persist_by_params ben = users(:ben) session = UserSession.new - - assert !session.persisting? + + refute session.persisting? set_params_for(ben) - - assert !session.persisting? - assert !session.unauthorized_record - assert !session.record + + refute session.persisting? + refute session.unauthorized_record + refute session.record assert_nil controller.session["user_credentials"] - + set_request_content_type("text/plain") - assert !session.persisting? - assert !session.unauthorized_record + refute session.persisting? + refute session.unauthorized_record assert_nil controller.session["user_credentials"] - + set_request_content_type("application/atom+xml") assert session.persisting? assert_equal ben, session.record - assert_nil controller.session["user_credentials"] # should not persist since this is single access - + + # should not persist since this is single access + assert_nil controller.session["user_credentials"] + set_request_content_type("application/rss+xml") assert session.persisting? assert_equal ben, session.unauthorized_record @@ -50,4 +58,4 @@ def test_persist_persist_by_params end end end -end \ No newline at end of file +end diff --git a/test/session_test/password_test.rb b/test/session_test/password_test.rb index fd6a62a2..cfc54318 100644 --- a/test/session_test/password_test.rb +++ b/test/session_test/password_test.rb @@ -1,73 +1,90 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module PasswordTest class ConfigTest < ActiveSupport::TestCase - def test_find_by_login_method - UserSession.find_by_login_method = "my_login_method" - assert_equal "my_login_method", UserSession.find_by_login_method - - UserSession.find_by_login_method "find_by_login" - assert_equal "find_by_login", UserSession.find_by_login_method + def test_find_by_login_method_is_deprecated + expected_warning = Regexp.new( + Regexp.escape(::Authlogic::Session::Base::E_DPR_FIND_BY_LOGIN_METHOD) + ) + + assert_output(nil, expected_warning) do + UserSession.find_by_login_method = "my_login_method" + end + assert_equal "my_login_method", UserSession.record_selection_method + + assert_output(nil, expected_warning) do + UserSession.find_by_login_method "find_by_login" + end + assert_equal "find_by_login", UserSession.record_selection_method end - + + def test_record_selection_method + UserSession.record_selection_method = "my_login_method" + assert_equal "my_login_method", UserSession.record_selection_method + + UserSession.record_selection_method "find_by_login" + assert_equal "find_by_login", UserSession.record_selection_method + end + def test_verify_password_method UserSession.verify_password_method = "my_login_method" assert_equal "my_login_method", UserSession.verify_password_method - + UserSession.verify_password_method "valid_password?" assert_equal "valid_password?", UserSession.verify_password_method end - + def test_generalize_credentials_error_mesages_set_to_false UserSession.generalize_credentials_error_messages false - assert !UserSession.generalize_credentials_error_messages - session = UserSession.create(:login => users(:ben).login, :password => "invalud-password") + refute UserSession.generalize_credentials_error_messages + session = UserSession.create(login: users(:ben).login, password: "invalud-password") assert_equal ["Password is not valid"], session.errors.full_messages end - + def test_generalize_credentials_error_messages_set_to_true UserSession.generalize_credentials_error_messages true assert UserSession.generalize_credentials_error_messages - session = UserSession.create(:login => users(:ben).login, :password => "invalud-password") + session = UserSession.create(login: users(:ben).login, password: "invalud-password") assert_equal ["Login/Password combination is not valid"], session.errors.full_messages end def test_generalize_credentials_error_messages_set_to_string - UserSession.generalize_credentials_error_messages= "Custom Error Message" + UserSession.generalize_credentials_error_messages = "Custom Error Message" assert UserSession.generalize_credentials_error_messages - session = UserSession.create(:login => users(:ben).login, :password => "invalud-password") + session = UserSession.create(login: users(:ben).login, password: "invalud-password") assert_equal ["Custom Error Message"], session.errors.full_messages end - def test_login_field UserSession.configured_password_methods = false UserSession.login_field = :saweet assert_equal :saweet, UserSession.login_field session = UserSession.new assert session.respond_to?(:saweet) - + UserSession.login_field :login assert_equal :login, UserSession.login_field session = UserSession.new assert session.respond_to?(:login) end - + def test_password_field UserSession.configured_password_methods = false UserSession.password_field = :saweet assert_equal :saweet, UserSession.password_field session = UserSession.new assert session.respond_to?(:saweet) - + UserSession.password_field :password assert_equal :password, UserSession.password_field session = UserSession.new assert session.respond_to?(:password) end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_init session = UserSession.new @@ -77,29 +94,30 @@ def test_init assert session.respond_to?(:password=) assert session.respond_to?(:protected_password, true) end - + def test_credentials session = UserSession.new - session.credentials = {:login => "login", :password => "pass"} + session.credentials = { login: "login", password: "pass" } assert_equal "login", session.login assert_nil session.password assert_equal "pass", session.send(:protected_password) - assert_equal({:password => "", :login => "login"}, session.credentials) + assert_equal({ password: "", login: "login" }, session.credentials) end - + def test_credentials_are_params_safe session = UserSession.new - assert_nothing_raised { session.credentials = {:hacker_method => "error!"} } + assert_nothing_raised { session.credentials = { hacker_method: "error!" } } end - + def test_save_with_credentials - ben = users(:ben) - session = UserSession.new(:login => ben.login, :password => "benrocks") + aaron = users(:aaron) + session = UserSession.new(login: aaron.login, password: "aaronrocks") assert session.save - assert !session.new_session? + refute session.new_session? assert_equal 1, session.record.login_count assert Time.now >= session.record.current_login_at assert_equal "1.1.1.1", session.record.current_login_ip + assert_equal env_session_options[:renew], true end end end diff --git a/test/session_test/perishability_test.rb b/test/session_test/perishability_test.rb index ec95cf3d..b92e501d 100644 --- a/test/session_test/perishability_test.rb +++ b/test/session_test/perishability_test.rb @@ -1,15 +1,17 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class PerishabilityTest < ActiveSupport::TestCase def test_after_save ben = users(:ben) old_perishable_token = ben.perishable_token - session = UserSession.create(ben) + UserSession.create(ben) assert_not_equal old_perishable_token, ben.perishable_token - + drew = employees(:drew) - assert UserSession.create(drew) + refute UserSession.create(drew).new_session? end end -end \ No newline at end of file +end diff --git a/test/session_test/persistence_test.rb b/test/session_test/persistence_test.rb index df16a4ef..9eaab650 100644 --- a/test/session_test/persistence_test.rb +++ b/test/session_test/persistence_test.rb @@ -1,21 +1,50 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" +require "authlogic/controller_adapters/rails_adapter" module SessionTest class PersistenceTest < ActiveSupport::TestCase def test_find - ben = users(:ben) - assert !UserSession.find - http_basic_auth_for(ben) { assert UserSession.find } - set_cookie_for(ben) + aaron = users(:aaron) + refute UserSession.find + UserSession.allow_http_basic_auth = true + http_basic_auth_for(aaron) { assert UserSession.find } + set_cookie_for(aaron) assert UserSession.find unset_cookie - set_session_for(ben) + set_session_for(aaron) session = UserSession.find assert session end - + + def test_find_in_api + @controller = Authlogic::TestCase::MockAPIController.new + UserSession.controller = + Authlogic::ControllerAdapters::RailsAdapter.new(@controller) + + aaron = users(:aaron) + refute UserSession.find + + UserSession.single_access_allowed_request_types = ["application/json"] + set_params_for(aaron) + set_request_content_type("application/json") + assert UserSession.find + end + def test_persisting # tested thoroughly in test_find end + + def test_should_set_remember_me_on_the_next_request + aaron = users(:aaron) + session = UserSession.new(aaron) + session.remember_me = true + refute UserSession.remember_me + assert session.save + assert session.remember_me? + session = UserSession.find(aaron) + assert session.remember_me? + end end -end \ No newline at end of file +end diff --git a/test/session_test/scopes_test.rb b/test/session_test/scopes_test.rb index 0b078851..4bc9dfe7 100644 --- a/test/session_test/scopes_test.rb +++ b/test/session_test/scopes_test.rb @@ -1,21 +1,23 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class ScopesTest < ActiveSupport::TestCase def test_scope_method assert_nil Authlogic::Session::Base.scope - + thread1 = Thread.new do - scope = {:id => :scope1} + scope = { id: :scope1 } Authlogic::Session::Base.send(:scope=, scope) assert_equal scope, Authlogic::Session::Base.scope end thread1.join - + assert_nil Authlogic::Session::Base.scope - + thread2 = Thread.new do - scope = {:id => :scope2} + scope = { id: :scope2 } Authlogic::Session::Base.send(:scope=, scope) assert_equal scope, Authlogic::Session::Base.scope end @@ -23,38 +25,44 @@ def test_scope_method assert_nil Authlogic::Session::Base.scope end - + def test_with_scope_method assert_raise(ArgumentError) { UserSession.with_scope } - - UserSession.with_scope(:find_options => {:conditions => "awesome = 1"}, :id => "some_id") do - assert_equal({:find_options => {:conditions => "awesome = 1"}, :id => "some_id"}, UserSession.scope) + + UserSession.with_scope(find_options: { conditions: "awesome = 1" }, id: "some_id") do + assert_equal( + { find_options: { conditions: "awesome = 1" }, id: "some_id" }, + UserSession.scope + ) end - + assert_nil UserSession.scope end - + def test_initialize - UserSession.with_scope(:find_options => {:conditions => "awesome = 1"}, :id => "some_id") do + UserSession.with_scope(find_options: { conditions: "awesome = 1" }, id: "some_id") do session = UserSession.new - assert_equal({:find_options => {:conditions => "awesome = 1"}, :id => "some_id"}, session.scope) + assert_equal( + { find_options: { conditions: "awesome = 1" }, id: "some_id" }, + session.scope + ) session.id = :another_id assert_equal "another_id_some_id_test", session.send(:build_key, "test") end end - + def test_search_for_record_with_scopes binary_logic = companies(:binary_logic) ben = users(:ben) zack = users(:zack) - + session = UserSession.new assert_equal zack, session.send(:search_for_record, "find_by_login", zack.login) - - session.scope = {:find_options => {:conditions => ["company_id = ?", binary_logic.id]}} + + session.scope = { find_options: { conditions: ["company_id = ?", binary_logic.id] } } assert_nil session.send(:search_for_record, "find_by_login", zack.login) - + assert_equal ben, session.send(:search_for_record, "find_by_login", ben.login) end end -end \ No newline at end of file +end diff --git a/test/session_test/session_test.rb b/test/session_test/session_test.rb index edae0b9d..6502a9ef 100644 --- a/test/session_test/session_test.rb +++ b/test/session_test/session_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module SessionTest @@ -6,12 +8,12 @@ class ConfigTest < ActiveSupport::TestCase def test_session_key UserSession.session_key = "my_session_key" assert_equal "my_session_key", UserSession.session_key - + UserSession.session_key "user_credentials" assert_equal "user_credentials", UserSession.session_key end end - + class InstanceMethodsTest < ActiveSupport::TestCase def test_persist_persist_by_session ben = users(:ben) @@ -19,25 +21,49 @@ def test_persist_persist_by_session assert session = UserSession.find assert_equal ben, session.record assert_equal ben.persistence_token, controller.session["user_credentials"] + refute_includes env_session_options, :renew + end + + # A SQL injection attack to steal the persistence_token. + # TODO: Explain how `:select` is used, and sanitized. + def test_persist_persist_by_session_with_sql_injection_attack + ben = users(:ben) + controller.session["user_credentials"] = "neo" + controller.session["user_credentials_id"] = { + select: " *,'neo' AS persistence_token FROM users WHERE id = #{ben.id} limit 1 -- " + } + @user_session = UserSession.find + assert @user_session.blank? + end + + def test_persist_persist_by_session_with_sql_injection_attack_2 + controller.session["user_credentials"] = { select: "ABRA CADABRA" } + controller.session["user_credentials_id"] = nil + assert_nothing_raised do + @user_session = UserSession.find + end + assert @user_session.blank? end - + def test_persist_persist_by_session_with_token_only ben = users(:ben) set_session_for(ben) controller.session["user_credentials_id"] = nil - assert session = UserSession.find + session = UserSession.find assert_equal ben, session.record assert_equal ben.persistence_token, controller.session["user_credentials"] + refute_includes env_session_options, :renew end - + def test_after_save_update_session ben = users(:ben) session = UserSession.new(ben) assert controller.session["user_credentials"].blank? assert session.save assert_equal ben.persistence_token, controller.session["user_credentials"] + assert_equal env_session_options[:renew], true end - + def test_after_destroy_update_session ben = users(:ben) set_session_for(ben) @@ -45,15 +71,17 @@ def test_after_destroy_update_session assert session = UserSession.find assert session.destroy assert controller.session["user_credentials"].blank? + refute_includes env_session_options, :renew end - + def test_after_persisting_update_session ben = users(:ben) set_cookie_for(ben) assert controller.session["user_credentials"].blank? assert UserSession.find assert_equal ben.persistence_token, controller.session["user_credentials"] + refute_includes env_session_options, :renew end end end -end \ No newline at end of file +end diff --git a/test/session_test/timeout_test.rb b/test/session_test/timeout_test.rb index 17e0b1a8..3b1acc2b 100644 --- a/test/session_test/timeout_test.rb +++ b/test/session_test/timeout_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest module TimeoutTest @@ -6,12 +8,12 @@ class ConfigTest < ActiveSupport::TestCase def test_logout_on_timeout UserSession.logout_on_timeout = true assert UserSession.logout_on_timeout - + UserSession.logout_on_timeout false - assert !UserSession.logout_on_timeout + refute UserSession.logout_on_timeout end end - + class InstanceMethods < ActiveSupport::TestCase def test_stale_state UserSession.logout_on_timeout = true @@ -19,34 +21,64 @@ def test_stale_state ben.last_request_at = 3.years.ago ben.save set_session_for(ben) - + session = UserSession.new assert session.persisting? assert session.stale? assert_equal ben, session.stale_record assert_nil session.record assert_nil controller.session["user_credentials_id"] - + set_session_for(ben) - + ben.last_request_at = Time.now ben.save - + assert session.persisting? - assert !session.stale? + refute session.stale? assert_nil session.stale_record - + UserSession.logout_on_timeout = false end - + + def test_should_be_stale_with_expired_remember_date + UserSession.logout_on_timeout = true + UserSession.remember_me = true + UserSession.remember_me_for = 3.months + ben = users(:ben) + assert ben.save + session = UserSession.new(ben) + assert session.save + Timecop.freeze(Time.now + 4.month) + assert session.persisting? + assert session.stale? + UserSession.remember_me = false + end + + def test_should_not_be_stale_with_valid_remember_date + UserSession.logout_on_timeout = true # Default is 10.minutes + UserSession.remember_me = true + UserSession.remember_me_for = 3.months + ben = users(:ben) + assert ben.save + session = UserSession.new(ben) + assert session.save + Timecop.freeze(Time.now + 2.months) + assert session.persisting? + refute session.stale? + UserSession.remember_me = false + end + def test_successful_login UserSession.logout_on_timeout = true ben = users(:ben) - assert UserSession.create(:login => ben.login, :password => "benrocks") - assert session = UserSession.find + session = UserSession.create(login: ben.login, password: "benrocks") + refute session.new_session? + session = UserSession.find + assert session assert_equal ben, session.record UserSession.logout_on_timeout = false end end end -end \ No newline at end of file +end diff --git a/test/session_test/unauthorized_record_test.rb b/test/session_test/unauthorized_record_test.rb index 7e1181da..61d75cef 100644 --- a/test/session_test/unauthorized_record_test.rb +++ b/test/session_test/unauthorized_record_test.rb @@ -1,4 +1,6 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class UnauthorizedRecordTest < ActiveSupport::TestCase @@ -7,7 +9,7 @@ def test_credentials session = UserSession.new session.credentials = [ben] assert_equal ben, session.unauthorized_record - assert_equal({:unauthorized_record => ""}, session.credentials) + assert_equal({ unauthorized_record: "" }, session.credentials) end end -end \ No newline at end of file +end diff --git a/test/session_test/validation_test.rb b/test/session_test/validation_test.rb index 632b2eea..321a289c 100644 --- a/test/session_test/validation_test.rb +++ b/test/session_test/validation_test.rb @@ -1,18 +1,20 @@ -require 'test_helper' +# frozen_string_literal: true + +require "test_helper" module SessionTest class ValidationTest < ActiveSupport::TestCase def test_errors session = UserSession.new - assert session.errors.is_a?(Authlogic::Session::Validation::Errors) + assert_kind_of ::ActiveModel::Errors, session.errors end - + def test_valid session = UserSession.new - assert !session.valid? + refute session.valid? assert_nil session.record assert session.errors.count > 0 - + ben = users(:ben) session.unauthorized_record = ben assert session.valid? @@ -20,4 +22,4 @@ def test_valid assert session.errors.empty? end end -end \ No newline at end of file +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 35438592..30c9561f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,53 +1,95 @@ -require "test/unit" +# frozen_string_literal: true + +require "coveralls" +require "simplecov" +require "simplecov-console" + +Coveralls.wear! + +SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( + [ + SimpleCov::Formatter::Console, + # Want a nice code coverage website? Uncomment this next line! + SimpleCov::Formatter::HTMLFormatter + ] +) +SimpleCov.start { add_filter "/test/" } + +require "byebug" require "rubygems" -require "ruby-debug" +require "minitest/autorun" require "active_record" require "active_record/fixtures" +require "timecop" +require "i18n" +require "minitest/reporters" -# A temporary fix to bring active record errors up to speed with rails edge. -# I need to remove this once the new gem is released. This is only here so my tests pass. -unless defined?(::ActiveModel) - class ActiveRecord::Errors - def [](key) - value = on(key) - value.is_a?(Array) ? value : [value].compact - end - end +Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new) + +I18n.load_path << File.dirname(__FILE__) + "/i18n/lol.yml" + +case ENV["DB"] +when "mysql" + ActiveRecord::Base.establish_connection( + adapter: "mysql2", + database: ENV.fetch("AUTHLOGIC_DB_NAME", "authlogic"), + host: ENV.fetch("AUTHLOGIC_DB_HOST", nil), + port: ENV.fetch("AUTHLOGIC_DB_PORT", nil), + username: ENV.fetch("AUTHLOGIC_DB_USER", "root") + ) +when "postgres" + ActiveRecord::Base.establish_connection( + adapter: "postgresql", + database: ENV.fetch("AUTHLOGIC_DB_NAME", "authlogic"), + host: ENV.fetch("AUTHLOGIC_DB_HOST", nil), + password: ENV.fetch("AUTHLOGIC_DB_PASSWORD", nil), + port: ENV.fetch("AUTHLOGIC_DB_PORT", nil), + username: ENV.fetch("AUTHLOGIC_DB_USER", nil) + ) +else + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") end +logger = Logger.new(STDOUT) +logger.level = Logger::FATAL +ActiveRecord::Base.logger = logger -ActiveRecord::Schema.verbose = false +if ActiveRecord::VERSION::STRING < "4.1" + ActiveRecord::Base.configurations = true +end -begin - ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:") -rescue ArgumentError - ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:") +if ActiveSupport.respond_to?(:test_order) + ActiveSupport.test_order = :sorted end -ActiveRecord::Base.configurations = true -ActiveRecord::Schema.define(:version => 1) do +if ActiveRecord::VERSION::STRING < "7.1" + ActiveRecord::Base.default_timezone = :local +else + ActiveRecord.default_timezone = :local +end +ActiveRecord::Schema.define(version: 1) do create_table :companies do |t| - t.datetime :created_at - t.datetime :updated_at + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 t.string :name t.boolean :active end create_table :projects do |t| - t.datetime :created_at - t.datetime :updated_at + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 t.string :name end - - create_table :projects_users, :id => false do |t| + + create_table :projects_users, id: false do |t| t.integer :project_id t.integer :user_id end - + create_table :users do |t| - t.datetime :created_at - t.datetime :updated_at - t.integer :lock_version, :default => 0 + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 + t.integer :lock_version, default: 0 t.integer :company_id t.string :login t.string :crypted_password @@ -58,21 +100,21 @@ def [](key) t.string :email t.string :first_name t.string :last_name - t.integer :login_count, :default => 0, :null => false - t.integer :failed_login_count, :default => 0, :null => false - t.datetime :last_request_at - t.datetime :current_login_at - t.datetime :last_login_at + t.integer :login_count, default: 0, null: false + t.integer :failed_login_count, default: 0, null: false + t.datetime :last_request_at, limit: 6 + t.datetime :current_login_at, limit: 6 + t.datetime :last_login_at, limit: 6 t.string :current_login_ip t.string :last_login_ip - t.boolean :active, :default => true - t.boolean :approved, :default => true - t.boolean :confirmed, :default => true + t.boolean :active, default: true + t.boolean :approved, default: true + t.boolean :confirmed, default: true end - + create_table :employees do |t| - t.datetime :created_at - t.datetime :updated_at + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 t.integer :company_id t.string :email t.string :crypted_password @@ -80,103 +122,202 @@ def [](key) t.string :persistence_token t.string :first_name t.string :last_name - t.integer :login_count, :default => 0, :null => false - t.datetime :last_request_at - t.datetime :current_login_at - t.datetime :last_login_at + t.integer :login_count, default: 0, null: false + t.datetime :last_request_at, limit: 6 + t.datetime :current_login_at, limit: 6 + t.datetime :last_login_at, limit: 6 t.string :current_login_ip t.string :last_login_ip end - + create_table :affiliates do |t| - t.datetime :created_at - t.datetime :updated_at + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 t.integer :company_id t.string :username t.string :pw_hash t.string :pw_salt t.string :persistence_token end - + create_table :ldapers do |t| - t.datetime :created_at - t.datetime :updated_at + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 t.string :ldap_login t.string :persistence_token end + + create_table :admins do |t| + t.datetime :created_at, limit: 6 + t.datetime :updated_at, limit: 6 + t.string :login + t.string :crypted_password + t.string :password_salt + t.string :persistence_token + t.string :perishable_token + t.string :email + t.string :role + end end -require File.dirname(__FILE__) + '/../lib/authlogic' unless defined?(Authlogic) -require File.dirname(__FILE__) + '/../lib/authlogic/test_case' -require File.dirname(__FILE__) + '/libs/project' -require File.dirname(__FILE__) + '/libs/affiliate' -require File.dirname(__FILE__) + '/libs/employee' -require File.dirname(__FILE__) + '/libs/employee_session' -require File.dirname(__FILE__) + '/libs/ldaper' -require File.dirname(__FILE__) + '/libs/user' -require File.dirname(__FILE__) + '/libs/user_session' -require File.dirname(__FILE__) + '/libs/company' - -Authlogic::CryptoProviders::AES256.key = "myafdsfddddddddddddddddddddddddddddddddddddddddddddddd" - -class ActiveSupport::TestCase - include ActiveRecord::TestFixtures - self.fixture_path = File.dirname(__FILE__) + "/fixtures" - self.use_transactional_fixtures = false - self.use_instantiated_fixtures = false - self.pre_loaded_fixtures = false - fixtures :all - setup :activate_authlogic - - private +require "English" +$LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) +require "authlogic" +require "authlogic/test_case" + +# Configure SCrypt to be as fast as possible. This is desirable for a test +# suite, and would be the opposite of desirable for production. +Authlogic::CryptoProviders::SCrypt.max_time = 0.001 # 1ms +Authlogic::CryptoProviders::SCrypt.max_mem = 1024 * 1024 # 1MB, the minimum SCrypt allows + +require "libs/project" +require "libs/affiliate" +require "libs/employee" +require "libs/employee_session" +require "libs/ldaper" +require "libs/user" +require "libs/user_session" +require "libs/company" +require "libs/admin" + +module ActiveSupport + class TestCase + include ActiveRecord::TestFixtures + + # `fixture_path=` was deprecated in favor of + # `fixture_paths=` in Rails 7.1, removed in Rails 7.2. + if respond_to?(:fixture_paths=) + self.fixture_paths = [File.dirname(__FILE__) + "/fixtures"] + else + self.fixture_path = File.dirname(__FILE__) + "/fixtures" + end + + self.use_transactional_tests = false + self.use_instantiated_fixtures = false + self.pre_loaded_fixtures = false + fixtures :all + setup :activate_authlogic + setup :config_setup + teardown :config_teardown + teardown { Timecop.return } # for tests that need to freeze the time + + private + + # Many of the tests change Authlogic config for the test models. Some tests + # were not resetting the config after tests, which didn't surface as broken + # tests until Rails 4.1 was added for testing. This ensures that all the + # models start tests with their original config. + def config_setup + [ + Project, + Affiliate, + Employee, + EmployeeSession, + Ldaper, + User, + UserSession, + Company, + Admin + ].each do |model| + unless model.respond_to?(:original_acts_as_authentic_config) + model.class_attribute :original_acts_as_authentic_config + end + model.original_acts_as_authentic_config = model.acts_as_authentic_config + end + end + + def config_teardown + [ + Project, + Affiliate, + Employee, + EmployeeSession, + Ldaper, + User, + UserSession, + Company, + Admin + ].each do |model| + model.acts_as_authentic_config = model.original_acts_as_authentic_config + end + end + + def env_session_options + controller.request.env.fetch( + ::Authlogic::ControllerAdapters::AbstractAdapter::ENV_SESSION_OPTIONS, + {} + ).with_indifferent_access + end + def password_for(user) case user when users(:ben) "benrocks" when users(:zack) "zackrocks" + when users(:aaron) + "aaronrocks" end end - - def http_basic_auth_for(user = nil, &block) + + def http_basic_auth_for(user = nil) unless user.blank? controller.http_user = user.login controller.http_password = password_for(user) end yield - controller.http_user = controller.http_password = nil + controller.http_user = controller.http_password = controller.realm = nil end - - def set_cookie_for(user, id = nil) - controller.cookies["user_credentials"] = {:value => user.persistence_token, :expires => nil} + + def set_cookie_for(user) + controller.cookies["user_credentials"] = { + value: "#{user.persistence_token}::#{user.id}", + expires: nil + } end - + def unset_cookie controller.cookies["user_credentials"] = nil end - - def set_params_for(user, id = nil) + + def set_params_for(user) controller.params["user_credentials"] = user.single_access_token end - + def unset_params controller.params["user_credentials"] = nil end - + def set_request_content_type(type) controller.request_content_type = type end - + def unset_request_content_type controller.request_content_type = nil end - - def set_session_for(user, id = nil) - controller.session["user_credentials"] = user.persistence_token - controller.session["user_credentials_id"] = user.id + + def session_credentials_prefix(scope_record) + if scope_record.nil? + "" + else + format( + "%s_%d_", + scope_record.class.model_name.name.underscore, + scope_record.id + ) + end + end + + # Sets the session variables that `record` (eg. a `User`) would have after + # logging in. + def set_session_for(record) + record_class_name = record.class.model_name.name.underscore + controller.session["#{record_class_name}_credentials"] = record.persistence_token + controller.session["#{record_class_name}_credentials_id"] = record.id end - + def unset_session controller.session["user_credentials"] = controller.session["user_credentials_id"] = nil end + end end