├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_proposal.md ├── triage.md └── workflows │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── UPGRADING.md ├── authlogic.gemspec ├── doc ├── rails_support_in_authlogic_5.0.md └── use_normal_rails_validation.md ├── gemfiles ├── rails_5.2.rb ├── rails_6.0.rb ├── rails_6.1.rb ├── rails_7.0.rb ├── rails_7.1.rb ├── rails_7.2.rb └── rails_8.0.rb ├── lib ├── authlogic.rb └── authlogic │ ├── acts_as_authentic │ ├── base.rb │ ├── email.rb │ ├── logged_in_status.rb │ ├── login.rb │ ├── magic_columns.rb │ ├── password.rb │ ├── perishable_token.rb │ ├── persistence_token.rb │ ├── queries │ │ ├── case_sensitivity.rb │ │ └── find_with_case.rb │ ├── session_maintenance.rb │ └── single_access_token.rb │ ├── config.rb │ ├── controller_adapters │ ├── abstract_adapter.rb │ ├── rack_adapter.rb │ ├── rails_adapter.rb │ └── sinatra_adapter.rb │ ├── cookie_credentials.rb │ ├── crypto_providers.rb │ ├── crypto_providers │ ├── bcrypt.rb │ ├── md5.rb │ ├── md5 │ │ └── v2.rb │ ├── scrypt.rb │ ├── sha1.rb │ ├── sha1 │ │ └── v2.rb │ ├── sha256.rb │ ├── sha256 │ │ └── v2.rb │ ├── sha512.rb │ └── sha512 │ │ └── v2.rb │ ├── errors.rb │ ├── i18n.rb │ ├── i18n │ └── translator.rb │ ├── random.rb │ ├── session │ ├── base.rb │ └── magic_column │ │ └── assigns_last_request_at.rb │ ├── test_case.rb │ ├── test_case │ ├── mock_api_controller.rb │ ├── mock_controller.rb │ ├── mock_cookie_jar.rb │ ├── mock_logger.rb │ ├── mock_request.rb │ └── rails_request_adapter.rb │ └── version.rb └── test ├── acts_as_authentic_test ├── base_test.rb ├── email_test.rb ├── logged_in_status_test.rb ├── login_test.rb ├── magic_columns_test.rb ├── password_test.rb ├── perishable_token_test.rb ├── persistence_token_test.rb ├── session_maintenance_test.rb └── single_access_test.rb ├── adapter_test.rb ├── config_test.rb ├── crypto_provider_test ├── bcrypt_test.rb ├── md5 │ └── v2_test.rb ├── md5_test.rb ├── scrypt_test.rb ├── sha1 │ └── v2_test.rb ├── sha1_test.rb ├── sha256 │ └── v2_test.rb ├── sha256_test.rb ├── sha512 │ └── v2_test.rb └── sha512_test.rb ├── fixtures ├── admins.yml ├── companies.yml ├── employees.yml ├── projects.yml └── users.yml ├── i18n └── lol.yml ├── i18n_test.rb ├── libs ├── admin.rb ├── admin_session.rb ├── affiliate.rb ├── company.rb ├── employee.rb ├── employee_session.rb ├── ldaper.rb ├── project.rb ├── user.rb └── user_session.rb ├── random_test.rb ├── session_test ├── activation_test.rb ├── active_record_trickery_test.rb ├── brute_force_protection_test.rb ├── callbacks_test.rb ├── cookies_test.rb ├── credentials_test.rb ├── existence_test.rb ├── foundation_test.rb ├── http_auth_test.rb ├── id_test.rb ├── klass_test.rb ├── magic_columns_test.rb ├── magic_states_test.rb ├── params_test.rb ├── password_test.rb ├── perishability_test.rb ├── persistence_test.rb ├── scopes_test.rb ├── session_test.rb ├── timeout_test.rb ├── unauthorized_record_test.rb └── validation_test.rb └── test_helper.rb /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I want to fix a bug, but need some help 3 | about: > 4 | If the bug is easy to reproduce, we will help. However, you must fix the bug, 5 | in a reasonable amount of time, or your issue will be closed. See 6 | CONTRIBUTING.md 7 | 8 | --- 9 | 10 | ISSUES THAT DO NOT FOLLOW THIS TEMPLATE WILL BE CLOSED IMMEDIATELY. 11 | 12 | - [ ] This is not a usage question. 13 | - Our volunteers' time is limited, so please ask usage questions on 14 | [StackOverflow](http://stackoverflow.com/questions/tagged/authlogic). 15 | - [ ] This is not a security issue. 16 | - Do not disclose security issues in public. See our [contributing 17 | guide](https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md) 18 | for instructions. 19 | - [ ] This bug is reproducible with a clean install of authlogic 20 | - [ ] I am committed to fixing this in a reasonable amount of time, and 21 | responding promptly to feedback. 22 | 23 | # Expected Behavior 24 | 25 | Describe. 26 | 27 | # Actual Behavior 28 | 29 | Describe. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a usage question 4 | url: http://stackoverflow.com/questions/tagged/authlogic 5 | about: Due to limited volunteers, we cannot answer usage questions on GitHub. 6 | - name: I found a security vulnerability 7 | url: https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md 8 | about: Please email the maintainers as described in CONTRIBUTING.md 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Proposal 3 | about: > 4 | Propose something that you would like to build. We'll help, but you must build 5 | it yourself, in a reasonable amount of time, or your issue will be closed. See 6 | CONTRIBUTING.md 7 | 8 | --- 9 | 10 | ISSUES THAT DO NOT FOLLOW THIS TEMPLATE WILL BE CLOSED IMMEDIATELY. 11 | 12 | - [ ] This is not a usage question. 13 | - Our volunteers' time is limited, so please ask usage questions on 14 | [StackOverflow](http://stackoverflow.com/questions/tagged/authlogic). 15 | - [ ] This is not a security issue. 16 | - Do not disclose security issues in public. See our [contributing 17 | guide](https://github.com/binarylogic/authlogic/blob/master/CONTRIBUTING.md) 18 | for instructions. 19 | - [ ] I am committed to implementing this feature in a reasonable amount of 20 | time, and responding promptly to feedback. 21 | 22 | # Current Behavior 23 | 24 | Describe. 25 | 26 | # Proposed Behavior 27 | 28 | Describe. 29 | 30 | # Proposed Solution 31 | 32 | It's OK if you don't have a solution, we can help with that. But, whatever 33 | solution we decide on together, you must build yourself. 34 | -------------------------------------------------------------------------------- /.github/triage.md: -------------------------------------------------------------------------------- 1 | # Triage 2 | 3 | Common responses to issues. 4 | 5 | ## Ignored issue template, bug report 6 | 7 | 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. 8 | 9 | ## Usage Question 10 | 11 | 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. 12 | 13 | ## Usage question we were able to answer 14 | 15 | ``` 16 | If that doesn't answer your question, please ask a new question 17 | on [stackoverflow][1]. Unfortunatley, we just don't have enough volunteers to 18 | handle usage questions on github. 19 | 20 | Also, please check the [reference documentation][2]. You might find something 21 | there that's not in the readme. 22 | 23 | Thanks! 24 | 25 | [1]: http://stackoverflow.com/questions/tagged/authlogic 26 | [2]: https://github.com/binarylogic/authlogic#1c-reference-documentation 27 | ``` 28 | 29 | ## Old issue, generic 30 | 31 | ``` 32 | Hello, I'm going through old authlogic issues and seeing what to do with them. 33 | Skimming through this, it's unclear if it's a usage question, a feature 34 | suggestion, or a bug report. 35 | 36 | If this is a bug report, and you can still reproduce this issue with a clean 37 | install of the latest version of authlogic and rails (currently 3.6.0 and 5.1.4 38 | respectively), please create a git repo with a sample app that reproduces the 39 | problem, and open a new issue. 40 | 41 | If this is a feature suggestion, it's still relevant, and you are committed to 42 | implementing it, please open a new issue and we can discuss your implementation 43 | plan. 44 | 45 | If this is a usage question, please ask it on [stackoverflow][1]. Unfortunatley, 46 | we just don't have enough volunteers to handle usage questions on github. Also, 47 | please check the [reference documentation][2]. You might find something there 48 | that's not in the readme. 49 | 50 | Thanks! 51 | 52 | [1]: http://stackoverflow.com/questions/tagged/authlogic 53 | [2]: https://github.com/binarylogic/authlogic#1c-reference-documentation 54 | ``` 55 | 56 | ## Old issue, usage question / feature suggestion 57 | 58 | ``` 59 | Hello, I'm going through old authlogic issues and seeing what to do with them. 60 | This one looks a bit like a usage question and a bit like a feature suggestion. 61 | 62 | If this is a feature suggestion, it's still relevant, and you are committed to 63 | implementing it, please open a new issue and we can discuss your implementation 64 | plan. 65 | 66 | If this is a usage question, please ask it on [stackoverflow][1]. Unfortunately, 67 | we just don't have enough volunteers to handle usage questions on github. Also, 68 | please check the [reference documentation][2]. You might find something there 69 | that's not in the readme. 70 | 71 | Thanks! 72 | 73 | [1]: http://stackoverflow.com/questions/tagged/authlogic 74 | [2]: https://github.com/binarylogic/authlogic#1c-reference-documentation 75 | ``` 76 | 77 | ## Old issue, bug report 78 | 79 | ``` 80 | Hello, I'm going through old authlogic issues and seeing what to do with them. 81 | This one looks like a bug report. 82 | 83 | If you can still reproduce this issue with a clean install of the latest version 84 | of authlogic and rails, and you are committed to fixing it, please open a new issue. 85 | 86 | If this was more of a usage question than a bug report, please ask your question 87 | on [stackoverflow][1]. Unfortunately, we just don't have enough volunteers to 88 | handle usage questions on github. 89 | 90 | Thanks! 91 | 92 | [1]: http://stackoverflow.com/questions/tagged/authlogic 93 | ``` 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: gha-workflow-authlogic-test 2 | on: [push, pull_request] 3 | jobs: 4 | # Linting is a separate job, primary because it only needs to be done once, 5 | # and secondarily because jobs are performed concurrently. 6 | gha-job-authlogic-lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout source 11 | uses: actions/checkout@v4 12 | - name: Setup ruby 13 | uses: ruby/setup-ruby@v1 14 | with: 15 | # Set to `TargetRubyVersion` in `.rubocop.yml` 16 | ruby-version: 2.6 17 | - name: Bundle 18 | run: | 19 | gem install bundler -v 2.4.22 20 | bundle install --jobs 4 --retry 3 21 | - name: Lint 22 | run: bundle exec rubocop 23 | 24 | # The test job is a matrix of ruby/rails versions. 25 | gha-job-authlogic-test: 26 | name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }} 27 | runs-on: ubuntu-latest 28 | services: 29 | gha-service-authlogic-mysql: 30 | env: 31 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 32 | MYSQL_DATABASE: authlogic 33 | image: mysql:8.0 34 | options: >- 35 | --health-cmd="mysqladmin ping" 36 | --health-interval=10s 37 | --health-timeout=5s 38 | --health-retries=3 39 | ports: 40 | - 3306:3306 41 | gha-service-authlogic-postgres: 42 | env: 43 | POSTGRES_PASSWORD: asdfasdf 44 | image: postgres 45 | options: >- 46 | --health-cmd pg_isready 47 | --health-interval 10s 48 | --health-timeout 5s 49 | --health-retries 5 50 | ports: 51 | - 5432:5432 52 | strategy: 53 | fail-fast: false 54 | # Unlike TravisCI, the database will not be part of the matrix. Each 55 | # sub-job in the matrix tests all three databases. Alternatively, we could 56 | # have set this up with each database as a separate job, but then we'd be 57 | # duplicating the matrix configuration three times. 58 | matrix: 59 | # To keep matrix size down, only test highest and lowest rubies. In 60 | # `.rubocop.yml`, set `TargetRubyVersion`, to the lowest ruby version 61 | # tested here. 62 | ruby: ["3.1", "3.2", "3.3", "3.4"] 63 | rails: ["7.0", "7.1", "7.2", "8.0"] 64 | exclude: 65 | # rails 7 requires ruby >= 2.7.0 66 | - rails: "7.0" 67 | ruby: "3.1" 68 | - rails: "7.0" 69 | ruby: "3.2" 70 | - rails: "7.0" 71 | ruby: "3.3" 72 | - rails: "7.0" 73 | ruby: "3.4" 74 | - rails: "8.0" 75 | ruby: "3.1" 76 | env: 77 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.rb 78 | steps: 79 | - name: Checkout source 80 | uses: actions/checkout@v4 81 | - name: Setup ruby 82 | uses: ruby/setup-ruby@v1 83 | with: 84 | ruby-version: ${{ matrix.ruby }} 85 | bundler-cache: true 86 | # MySQL db was created above, sqlite will be created during test suite, 87 | # when migrations occur, so we only need to create the postgres db. I 88 | # tried something like `cd .....dummy_app && ....db:create`, but couldn't 89 | # get that to work. 90 | - name: Create postgres database 91 | run: | 92 | createdb \ 93 | --host=$POSTGRES_HOST \ 94 | --port=$POSTGRES_PORT \ 95 | --username=postgres \ 96 | authlogic 97 | env: 98 | PGPASSWORD: asdfasdf 99 | POSTGRES_HOST: localhost 100 | POSTGRES_PORT: 5432 101 | 102 | # The following three steps finally run the tests. 103 | # We run `rake test` instead of the default task, which includes rubocop, 104 | # because rubocop is done (once!) in a separate job above. 105 | - name: Test, sqlite 106 | run: bundle exec rake test 107 | env: 108 | DB: sqlite 109 | - name: Test, mysql 110 | run: bundle exec rake test 111 | env: 112 | BACKTRACE: 1 113 | DB: mysql 114 | AUTHLOGIC_DB_NAME: authlogic 115 | AUTHLOGIC_DB_USER: root 116 | AUTHLOGIC_DB_HOST: 127.0.0.1 117 | AUTHLOGIC_DB_PORT: 3306 118 | - name: Test, postgres 119 | run: bundle exec rake test 120 | env: 121 | BACKTRACE: 1 122 | DB: postgres 123 | AUTHLOGIC_DB_NAME: authlogic 124 | AUTHLOGIC_DB_USER: postgres 125 | AUTHLOGIC_DB_HOST: 127.0.0.1 126 | AUTHLOGIC_DB_PORT: 5432 127 | AUTHLOGIC_DB_PASSWORD: asdfasdf 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .swp 3 | *.gem 4 | *.log 5 | *.sqlite3 6 | pkg/* 7 | coverage/* 8 | benchmarks/* 9 | .rvmrc 10 | gemfiles/*.lock 11 | .bundle 12 | Gemfile.lock 13 | .ruby-gemset 14 | .ruby-version 15 | .byebug_history 16 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | Exclude: 5 | # TravisCI runs `bundle install --path=${BUNDLE_PATH:-vendor/bundle}` 6 | # causing our bundle to be installed in `gemfiles/vendor/bundle`. 7 | # Regardless, we have no interest in linting files in our bundle :D 8 | - gemfiles/vendor/bundle/**/* 9 | # Specify lowest supported ruby version. If we committed our .ruby-version 10 | # file, we wouldn't have to specify this (https://bit.ly/2vNTsue), but we 11 | # don't commit that file because that would interfere with testing multiple 12 | # rubies on CI. 13 | # 14 | # Should be same as `ruby-version` in `.github/workflows/test.yml` 15 | TargetRubyVersion: 2.6 16 | 17 | # Avoid empty lines in methods, they are a sign the method is too big. 18 | Layout/EmptyLineAfterGuardClause: 19 | Enabled: false 20 | 21 | # Aim for 80, but 100 is OK. 22 | Layout/LineLength: 23 | Max: 100 24 | 25 | Layout/MultilineMethodCallIndentation: 26 | EnforcedStyle: indented 27 | 28 | Layout/MultilineOperationIndentation: 29 | EnforcedStyle: indented 30 | 31 | # Please use normal indentation when aligning parameters. 32 | # 33 | # Good: 34 | # 35 | # method_call( 36 | # a, 37 | # b 38 | # ) 39 | # 40 | # method_call(a, 41 | # b 42 | # ) 43 | # 44 | # Bad: 45 | # 46 | # method_call(a, 47 | # b) 48 | # 49 | # The latter is harder to maintain and uses too much horizontal space. 50 | Layout/ParameterAlignment: 51 | EnforcedStyle: with_fixed_indentation 52 | 53 | Metrics/AbcSize: 54 | Exclude: 55 | # In an ideal world tests would be held to the same ABC metric as production 56 | # code. In practice, time spent doing so is not nearly as valuable as 57 | # spending the same time improving production code. 58 | - test/**/* 59 | 60 | # Questionable value compared to metrics like AbcSize or CyclomaticComplexity. 61 | Metrics/BlockLength: 62 | Enabled: false 63 | 64 | # Questionable value compared to metrics like AbcSize or CyclomaticComplexity. 65 | Metrics/ClassLength: 66 | Enabled: false 67 | 68 | # Questionable value compared to metrics like AbcSize or CyclomaticComplexity. 69 | Metrics/MethodLength: 70 | Enabled: false 71 | 72 | # Questionable value compared to metrics like AbcSize or CyclomaticComplexity. 73 | Metrics/ModuleLength: 74 | Enabled: false 75 | 76 | # Sometimes prefixing a method name with get_ or set_ is a reasonable choice. 77 | Naming/AccessorMethodName: 78 | Enabled: false 79 | 80 | # Having a consistent delimiter, like EOS, improves reading speed. The delimiter 81 | # is syntactic noise, just like a quotation mark, and inconsistent naming would 82 | # hurt reading speed, just as inconsistent quoting would. 83 | Naming/HeredocDelimiterNaming: 84 | Enabled: false 85 | 86 | # Avoid single-line method definitions. 87 | Style/EmptyMethod: 88 | EnforcedStyle: expanded 89 | 90 | # Avoid annotated tokens except in desperately complicated format strings. 91 | # In 99% of format strings they actually make it less readable. 92 | Style/FormatStringToken: 93 | Enabled: false 94 | 95 | # Too subtle to lint. Guard clauses are great, use them if they help. 96 | Style/GuardClause: 97 | Enabled: false 98 | 99 | # Too subtle to lint. A multi-line conditional may improve readability, even if 100 | # a postfix conditional would satisfy `Metrics/LineLength`. 101 | Style/IfUnlessModifier: 102 | Enabled: false 103 | 104 | # Too subtle to lint. Use semantic style, but prefer `}.x` over `end.x`. 105 | Style/BlockDelimiters: 106 | Enabled: false 107 | 108 | # Use the nested style because it is safer. It is easier to make mistakes with 109 | # the compact style. 110 | Style/ClassAndModuleChildren: 111 | EnforcedStyle: nested 112 | 113 | Style/Documentation: 114 | Exclude: 115 | - 'test/**/*' 116 | 117 | # Both `module_function` and `extend_self` are legitimate. Most importantly, 118 | # they are different (http://bit.ly/2hSQAGm) 119 | Style/ModuleFunction: 120 | Enabled: false 121 | 122 | # `x > 0` is understood by more programmers than `x.positive?` 123 | Style/NumericPredicate: 124 | EnforcedStyle: comparison 125 | 126 | # Use slashes for most patterns. Use %r when it reduces backslash escaping. 127 | Style/RegexpLiteral: 128 | AllowInnerSlashes: false 129 | 130 | # We use words, like `$LOAD_PATH`, because they are much less confusing that 131 | # arcane symbols like `$:`. Unfortunately, we must then `require "English"` in 132 | # a few places, but it's worth it so that we can read our code. 133 | Style/SpecialGlobalVars: 134 | EnforcedStyle: use_english_names 135 | 136 | Style/StringLiterals: 137 | EnforcedStyle: double_quotes 138 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --exclude-limit 1000` 3 | # on 2020-03-24 00:02:43 -0400 using RuboCop version 0.80.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 7 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: EnforcedStyle, IndentationWidth. 12 | # SupportedStyles: with_first_argument, with_fixed_indentation 13 | Layout/ArgumentAlignment: 14 | Exclude: 15 | - 'lib/authlogic/acts_as_authentic/magic_columns.rb' 16 | - 'lib/authlogic/acts_as_authentic/single_access_token.rb' 17 | - 'test/libs/user.rb' 18 | 19 | # Offense count: 14 20 | # Cop supports --auto-correct. 21 | Lint/SendWithMixinArgument: 22 | Exclude: 23 | - 'lib/authlogic/acts_as_authentic/base.rb' 24 | - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' 25 | - 'lib/authlogic/test_case.rb' 26 | 27 | # Offense count: 3 28 | Metrics/AbcSize: 29 | Max: 16.03 30 | 31 | # Offense count: 5 32 | Style/ClassVars: 33 | Exclude: 34 | - 'lib/authlogic/i18n.rb' 35 | 36 | # Offense count: 4 37 | Style/MethodMissingSuper: 38 | Exclude: 39 | - 'lib/authlogic/controller_adapters/abstract_adapter.rb' 40 | - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' 41 | - 'lib/authlogic/test_case/mock_request.rb' 42 | 43 | # Offense count: 3 44 | Style/MissingRespondToMissing: 45 | Exclude: 46 | - 'lib/authlogic/controller_adapters/sinatra_adapter.rb' 47 | - 'lib/authlogic/test_case/mock_request.rb' 48 | 49 | # Offense count: 6 50 | # Cop supports --auto-correct. 51 | # Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods. 52 | # 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 53 | Style/TrivialAccessors: 54 | Exclude: 55 | - 'lib/authlogic/session/base.rb' 56 | 57 | Style/HashEachMethods: 58 | Enabled: false 59 | 60 | Style/HashTransformKeys: 61 | Enabled: false 62 | 63 | Style/HashTransformValues: 64 | Enabled: false 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ## 6.5.0 (2025-04-10) 11 | 12 | - Breaking Changes 13 | - None 14 | - Added 15 | - None 16 | - Fixed 17 | - [#770](https://github.com/binarylogic/authlogic/pull/770) - Adds support for Rails 7.2 and 8.0 18 | - [#777](https://github.com/binarylogic/authlogic/pull/777) - Loads authlogic once Active Record has successfully loaded 19 | 20 | ## 6.4.3 (2023-12-17) 21 | 22 | - Breaking Changes 23 | - None 24 | - Added 25 | - Rubygems MFA requirement for authors added to gemspec. 26 | - Fixed 27 | - [#767](https://github.com/binarylogic/authlogic/pull/767) - Adds support for Rails 7.1 28 | - [#769](https://github.com/binarylogic/authlogic/issues/769) - Fixed GH workflow 29 | 30 | ## 6.4.2 (2021-12-21) 31 | 32 | - Breaking Changes 33 | - None 34 | - Added 35 | - None 36 | - Fixed 37 | - [#743](https://github.com/binarylogic/authlogic/pull/743) - Fixed 38 | deprecation warning in Rails 7 re: `ActiveRecord::Base.default_timezone` 39 | - [#745](https://github.com/binarylogic/authlogic/pull/745) - Fixed more 40 | deprecation warnings in Rails 7 41 | 42 | ## 6.4.1 (2021-02-22) 43 | 44 | - Breaking Changes 45 | - None 46 | - Added 47 | - `Authlogic::Session::Base.session_fixation_defense` - Reset the Rack 48 | session ID after authentication, to protect against Session Fixation 49 | attacks. (https://guides.rubyonrails.org/security.html#session-fixation) 50 | Default: true 51 | - Fixed 52 | - None 53 | 54 | ## 6.4.0 (2020-12-22) 55 | 56 | - Breaking Changes 57 | - None 58 | - Added 59 | - [#734](https://github.com/binarylogic/authlogic/pull/734) - Support for 60 | string cookies when using TestCase and friends 61 | - Fixed 62 | - None 63 | 64 | ## 6.3.0 (2020-12-17) 65 | 66 | - Breaking Changes 67 | - None 68 | - Added 69 | - [#733](https://github.com/binarylogic/authlogic/pull/733) - Rails 6.1 support 70 | - `find_by_login_method` is deprecated in favor of `record_selection_method`, 71 | to avoid confusion with ActiveRecord's "Dynamic Finders". 72 | - Fixed 73 | - [#726](https://github.com/binarylogic/authlogic/issues/726) - Thread 74 | safety in `Authlogic::Session::Base.klass_name` 75 | 76 | ## 6.2.0 (2020-09-03) 77 | 78 | - Breaking Changes 79 | - None 80 | - Added 81 | - [#684](https://github.com/binarylogic/authlogic/pull/684) - Use cookies 82 | only when available. Support for `ActionController::API` 83 | - Fixed 84 | - [#725](https://github.com/binarylogic/authlogic/pull/725) - `NoMethodError` 85 | when setting `sign_cookie` or `encrypt_cookie` before `controller` is 86 | defined. 87 | 88 | ## 6.1.0 (2020-05-03) 89 | 90 | - Breaking Changes 91 | - None 92 | - Added 93 | - [#666](https://github.com/binarylogic/authlogic/pull/666) - 94 | Forwardported Authlogic::Session::Cookies.encrypt_cookie option 95 | - [#723](https://github.com/binarylogic/authlogic/pull/723) - 96 | Option to raise a `Authlogic::ModelSetupError` when your database is not 97 | configured correctly. 98 | - Fixed 99 | - None 100 | 101 | ## 6.0.0 (2020-03-23) 102 | 103 | - Breaking Changes, Major 104 | 105 | - There is no longer a default `crypto_provider`. We still recommend SCrypt, 106 | but don't want users of other providers to be forced to install it. You 107 | must now explicitly specify your `crypto_provider`, eg. in your `user.rb`. 108 | 109 | acts_as_authentic do |c| 110 | c.crypto_provider = ::Authlogic::CryptoProviders::SCrypt 111 | end 112 | 113 | To continue to use the `scrypt` gem, add it to your `Gemfile`. 114 | 115 | gem "scrypt", "~> 3.0" 116 | 117 | - Breaking Changes, Minor 118 | - To set your crypto provider, you must use `crypto_provider=`, not 119 | `crypto_provider`. The arity of the later has changed from -1 (one optional 120 | arg) to 0 (no arguments). 121 | - Added 122 | - [#702](https://github.com/binarylogic/authlogic/pull/702) - The ability to 123 | specify "None" as a valid SameSite attribute 124 | - Fixed 125 | - [#686](https://github.com/binarylogic/authlogic/pull/686) - Respect 126 | the `log_in_after_create` setting when creating a new logged-out user 127 | - [#668](https://github.com/binarylogic/authlogic/pull/668) - 128 | BCrypt user forced to load SCrypt 129 | - [#697](https://github.com/binarylogic/authlogic/issues/697) - Add V2 130 | CryptoProviders for MD5 and SHA schemes that fix key stretching by hashing 131 | the byte digests instead of the hex strings representing those digests 132 | - Dependencies 133 | - Drop support for ruby 2.3 (reached EOL on 2019-04-01) 134 | 135 | ## Previous major version 136 | 137 | See eg. the `5-1-stable` branch 138 | 139 | [1]: https://github.com/binarylogic/authlogic/blob/master/doc/use_normal_rails_validation.md 140 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Authlogic 2 | 3 | ## Issues 4 | 5 | ### Security Issues 6 | 7 | **Do not disclose security issues in public.** Instead, please email: 8 | 9 | ``` 10 | Ben Johnson , 11 | Tieg Zaharia , 12 | Jared Beck 13 | ``` 14 | 15 | We will review security issues promptly. 16 | 17 | ### Non-Security Issues 18 | 19 | Please use github issues only for bug reports and feature suggestions. 20 | 21 | ### Usage Questions 22 | 23 | Please ask usage questions on 24 | [Stack Overflow](http://stackoverflow.com/questions/tagged/authlogic). 25 | 26 | ## Development 27 | 28 | Most local development should be done using the oldest supported version of 29 | ruby. See `required_ruby_version` in the gemspec. 30 | 31 | ### Testing 32 | 33 | Tests can be run against different versions of Rails: 34 | 35 | ``` 36 | # Rails 5.2 37 | BUNDLE_GEMFILE=gemfiles/rails_5.2.rb bundle install 38 | BUNDLE_GEMFILE=gemfiles/rails_5.2.rb bundle exec rake 39 | 40 | # Rails 6.0 41 | BUNDLE_GEMFILE=gemfiles/rails_6.0.rb bundle install 42 | BUNDLE_GEMFILE=gemfiles/rails_6.0.rb bundle exec rake 43 | 44 | # Rails 6.1 45 | BUNDLE_GEMFILE=gemfiles/rails_6.1.rb bundle install 46 | BUNDLE_GEMFILE=gemfiles/rails_6.1.rb bundle exec rake 47 | 48 | # Rails 7.0 49 | BUNDLE_GEMFILE=gemfiles/rails_7.0.rb bundle install 50 | BUNDLE_GEMFILE=gemfiles/rails_7.0.rb bundle exec rake 51 | 52 | # Rails 7.1 53 | BUNDLE_GEMFILE=gemfiles/rails_7.1.rb bundle install 54 | BUNDLE_GEMFILE=gemfiles/rails_7.1.rb bundle exec rake 55 | 56 | # Rails 7.2 57 | BUNDLE_GEMFILE=gemfiles/rails_7.2.rb bundle install 58 | BUNDLE_GEMFILE=gemfiles/rails_7.2.rb bundle exec rake 59 | 60 | # Rails 8.0 61 | BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle install 62 | BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake 63 | ``` 64 | 65 | To run a single test: 66 | 67 | ``` 68 | BUNDLE_GEMFILE=gemfiles/rails_8.0.rb \ 69 | bundle exec ruby -I test path/to/test.rb 70 | ``` 71 | 72 | Bundler can be omitted, and the latest installed version of a gem dependency 73 | will be used. This is only suitable for certain unit tests. 74 | 75 | ``` 76 | ruby –I test path/to/test.rb 77 | ``` 78 | 79 | ### Test MySQL 80 | 81 | ``` 82 | mysql -e 'drop database authlogic; create database authlogic;' && \ 83 | DB=mysql BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake 84 | ``` 85 | 86 | ### Test PostgreSQL 87 | 88 | ``` 89 | psql -c 'create database authlogic;' -U postgres 90 | DB=postgres BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake 91 | ``` 92 | 93 | ### Linting 94 | 95 | Running `rake` also runs a linter, rubocop. Contributions must pass both 96 | the linter and the tests. The linter can be run on its own. 97 | 98 | ``` 99 | BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rubocop 100 | ``` 101 | 102 | To run the tests without linting, use `rake test`. 103 | 104 | ``` 105 | BUNDLE_GEMFILE=gemfiles/rails_8.0.rb bundle exec rake test 106 | ``` 107 | 108 | ### Version Control Branches 109 | 110 | We've been trying to follow the rails way, stable branches, but have been 111 | inconsistent. We should have one branch for each minor version, named like 112 | `4-3-stable`. Releases should be done on those branches, not in master. So, 113 | the "stable" branches should be the only branches with release tags. 114 | 115 | ### A normal release (no backport) 116 | 117 | 1. git checkout 4-3-stable # the latest "stable" branch (see above) 118 | 1. git merge master 119 | 1. Update version number in lib/authlogic/version.rb 120 | 1. In the changelog, 121 | - Add release date to entry 122 | - Add a new "Unreleased" section at top 123 | 1. In the readme, 124 | - Update version number in the docs table at the top 125 | - For non-patch versions, update the compatibility table 126 | 1. Commit with message like "Release 4.3.0" 127 | 1. git push origin 4-3-stable 128 | 1. CI should pass 129 | 1. gem build authlogic.gemspec 130 | 1. gem push authlogic-4.3.0.gem 131 | 1. git tag -a -m "v4.3.0" "v4.3.0" 132 | 1. git push --tags origin 4-3-stable 133 | 1. update the docs in the master branch, because that's what people look at 134 | - git checkout master 135 | - git merge --ff-only 4-3-stable 136 | - optional: amend commit, adding `[ci skip]` 137 | - git push 138 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Ben Johnson of Binary Logic 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubygems" 4 | require "bundler" 5 | 6 | Bundler.setup 7 | 8 | require "rake/testtask" 9 | Rake::TestTask.new(:test) do |test| 10 | test.libs << "test" 11 | test.pattern = "test/**/*_test.rb" 12 | test.verbose = false 13 | 14 | # Set interpreter warning level to 2 (verbose) 15 | test.ruby_opts << "-W2" 16 | end 17 | 18 | require "rubocop/rake_task" 19 | RuboCop::RakeTask.new 20 | 21 | task default: %i[rubocop test] 22 | 23 | require "coveralls/rake/task" 24 | Coveralls::RakeTask.new 25 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading Authlogic 2 | 3 | Supplemental instructions to complement CHANGELOG.md. 4 | 5 | ## 3.4.0 6 | 7 | In version 3.4.0, released 2014-03-03, the default crypto_provider was changed 8 | from *Sha512* to *SCrypt*. 9 | 10 | If you never set a crypto_provider and are upgrading, your passwords will break 11 | unless you specify `Sha512`. 12 | 13 | ``` ruby 14 | c.crypto_provider = Authlogic::CryptoProviders::Sha512 15 | ``` 16 | 17 | And if you want to automatically upgrade from *Sha512* to *SCrypt* as users login: 18 | 19 | ```ruby 20 | c.transition_from_crypto_providers = [Authlogic::CryptoProviders::Sha512] 21 | c.crypto_provider = Authlogic::CryptoProviders::SCrypt 22 | ``` 23 | -------------------------------------------------------------------------------- /authlogic.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "English" 4 | 5 | require_relative "lib/authlogic/version" 6 | 7 | ::Gem::Specification.new do |s| 8 | s.name = "authlogic" 9 | s.version = ::Authlogic.gem_version.to_s 10 | s.platform = ::Gem::Platform::RUBY 11 | s.authors = [ 12 | "Ben Johnson", 13 | "Tieg Zaharia", 14 | "Jared Beck" 15 | ] 16 | s.email = [ 17 | "bjohnson@binarylogic.com", 18 | "tieg.zaharia@gmail.com", 19 | "jared@jaredbeck.com" 20 | ] 21 | s.homepage = "https://github.com/binarylogic/authlogic" 22 | s.summary = "An unobtrusive ruby authentication library based on ActiveRecord." 23 | s.license = "MIT" 24 | s.metadata = { "rubygems_mfa_required" => "true" } 25 | s.required_ruby_version = ">= 2.6.0" 26 | 27 | # See doc/rails_support_in_authlogic_5.0.md 28 | s.add_dependency "activemodel", [">= 5.2", "< 8.1"] 29 | s.add_dependency "activerecord", [">= 5.2", "< 8.1"] 30 | s.add_dependency "activesupport", [">= 5.2", "< 8.1"] 31 | s.add_dependency "request_store", "~> 1.0" 32 | s.add_development_dependency "bcrypt", "~> 3.1" 33 | s.add_development_dependency "byebug", "~> 11.1.3" 34 | s.add_development_dependency "coveralls_reborn", "~> 0.28.0" 35 | s.add_development_dependency "minitest", "< 5.19.0" # See https://github.com/binarylogic/authlogic/issues/766 36 | s.add_development_dependency "minitest-reporters", "~> 1.3" 37 | s.add_development_dependency "mutex_m", "~> 0.3.0" 38 | s.add_development_dependency "rake", "~> 13.0" 39 | s.add_development_dependency "rubocop", "~> 0.80.1" 40 | s.add_development_dependency "rubocop-performance", "~> 1.1" 41 | s.add_development_dependency "scrypt", ">= 1.2", "< 4.0" 42 | s.add_development_dependency "simplecov", "~> 0.22.0" 43 | s.add_development_dependency "simplecov-console", "~> 0.9.1" 44 | s.add_development_dependency "timecop", "~> 0.7" 45 | 46 | # To reduce gem size, only the minimum files are included. 47 | # 48 | # Tests are intentionally excluded. We only support our own test suite, we do 49 | # not have enough volunteers to support "in-situ" testing. 50 | s.files = `git ls-files -z`.split("\x0").select { |f| 51 | f.match(%r{^(LICENSE|lib|authlogic.gemspec)/}) 52 | } 53 | s.test_files = [] # not packaged, see above 54 | s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 55 | s.require_paths = ["lib"] 56 | end 57 | -------------------------------------------------------------------------------- /doc/rails_support_in_authlogic_5.0.md: -------------------------------------------------------------------------------- 1 | # Rails support in Authlogic 5 2 | 3 | 2018-12-03 4 | 5 | Authlogic 5 adds support for rails 6.0, and drops support for rails < 5.2. 6 | This is in keeping with the [Maintenance Policy 7 | for Ruby on Rails](https://guides.rubyonrails.org/maintenance_policy.html). 8 | When rails 6 is released, rails < 5.2 will be EoL. Authlogic 5 reflects this. 9 | 10 | Authlogic 4 will continue to support rails `>= 4.2, < 5.3`. We'll continue to 11 | accept contributions to Authlogic 4 for as long as is practical. 12 | 13 | In conclusion, Authlogic 4 supports the "rails 5 era" and Authlogic 5 supports 14 | the "rails 6 era". 15 | -------------------------------------------------------------------------------- /doc/use_normal_rails_validation.md: -------------------------------------------------------------------------------- 1 | # Use Normal ActiveRecord Validation 2 | 3 | In Authlogic 4.4.0, [we deprecated][1] the features of Authlogic related to 4 | validating email, login, and password. In 5.0.0 these features will be dropped. 5 | Use normal ActiveRecord validations instead. 6 | 7 | ## Instructions for 4.4.0 8 | 9 | First, disable the deprecated Authlogic validations: 10 | 11 | acts_as_authentic do |c| 12 | c.validate_email_field = false 13 | c.validate_login_field = false 14 | c.validate_password_field = false 15 | end 16 | 17 | Then, use normal ActiveRecord validations instead. For example, instead of 18 | the Authlogic method validates_length_of_email_field_options, use 19 | 20 | validates :email, length: { ... } 21 | 22 | It might be a good idea to replace these one field at a time, ie. email, 23 | then login, then password; one field per commit. 24 | 25 | Finish this process before upgrading to Authlogic 5. 26 | 27 | ## Default Values 28 | 29 | The following validations represent the defaults in Authlogic 4. Merge these 30 | defaults with any settings you may have overwritten. 31 | 32 | ```ruby 33 | EMAIL = / 34 | \A 35 | [A-Z0-9_.&%+\-']+ # mailbox 36 | @ 37 | (?:[A-Z0-9\-]+\.)+ # subdomains 38 | (?:[A-Z]{2,25}) # TLD 39 | \z 40 | /ix 41 | LOGIN = /\A[a-zA-Z0-9_][a-zA-Z0-9\.+\-_@ ]+\z/ 42 | 43 | validates :email, 44 | format: { 45 | with: EMAIL, 46 | message: proc { 47 | ::Authlogic::I18n.t( 48 | "error_messages.email_invalid", 49 | default: "should look like an email address." 50 | ) 51 | } 52 | }, 53 | length: { maximum: 100 }, 54 | uniqueness: { 55 | case_sensitive: false, 56 | if: :will_save_change_to_email? 57 | } 58 | 59 | validates :login, 60 | format: { 61 | with: LOGIN, 62 | message: proc { 63 | ::Authlogic::I18n.t( 64 | "error_messages.login_invalid", 65 | default: "should use only letters, numbers, spaces, and .-_@+ please." 66 | ) 67 | } 68 | }, 69 | length: { within: 3..100 }, 70 | uniqueness: { 71 | case_sensitive: false, 72 | if: :will_save_change_to_login? 73 | } 74 | 75 | validates :password, 76 | confirmation: { if: :require_password? }, 77 | length: { 78 | minimum: 8, 79 | if: :require_password? 80 | } 81 | validates :password_confirmation, 82 | length: { 83 | minimum: 8, 84 | if: :require_password? 85 | } 86 | ``` 87 | 88 | ## Motivation 89 | 90 | The deprecated features save people some time in the beginning, when setting up 91 | Authlogic. But, later in the life of a project, when these settings need to 92 | change, it is obscure compared to normal ActiveRecord validations. 93 | 94 | [1]: https://github.com/binarylogic/authlogic/pull/623 95 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 5.2.0" 7 | gem "activesupport", "~> 5.2.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 1.4.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 6.0.0" 7 | gem "activesupport", "~> 6.0.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 1.4.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 6.1.0" 7 | gem "activesupport", "~> 6.1.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 1.4.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 7.0.0" 7 | gem "activesupport", "~> 7.0.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 1.6.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 7.1.0" 7 | gem "activesupport", "~> 7.1.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 1.6.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_7.2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 7.2.0" 7 | gem "activesupport", "~> 7.2.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 2.0.0" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_8.0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec path: ".." 5 | 6 | gem "activerecord", "~> 8.0.0" 7 | gem "activesupport", "~> 8.0.0" 8 | gem "mysql2", "~> 0.5.6" 9 | gem "pg", "~> 1.5.8" 10 | gem "sqlite3", "~> 2.1.0" 11 | -------------------------------------------------------------------------------- /lib/authlogic.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "authlogic/errors" 4 | require_relative "authlogic/i18n" 5 | require_relative "authlogic/random" 6 | require_relative "authlogic/config" 7 | 8 | require_relative "authlogic/controller_adapters/abstract_adapter" 9 | require_relative "authlogic/cookie_credentials" 10 | 11 | require_relative "authlogic/crypto_providers" 12 | 13 | require_relative "authlogic/acts_as_authentic/email" 14 | require_relative "authlogic/acts_as_authentic/logged_in_status" 15 | require_relative "authlogic/acts_as_authentic/login" 16 | require_relative "authlogic/acts_as_authentic/magic_columns" 17 | require_relative "authlogic/acts_as_authentic/password" 18 | require_relative "authlogic/acts_as_authentic/perishable_token" 19 | require_relative "authlogic/acts_as_authentic/persistence_token" 20 | require_relative "authlogic/acts_as_authentic/session_maintenance" 21 | require_relative "authlogic/acts_as_authentic/single_access_token" 22 | require_relative "authlogic/acts_as_authentic/base" 23 | 24 | require_relative "authlogic/session/magic_column/assigns_last_request_at" 25 | require_relative "authlogic/session/base" 26 | 27 | # Authlogic uses ActiveSupport's core extensions like `strip_heredoc` and 28 | # `squish`. ActiveRecord does not `require` these exensions, so we must. 29 | # 30 | # It's possible that we could save a few milliseconds by loading only the 31 | # specific core extensions we need, but `all.rb` is simpler. We can revisit this 32 | # decision if it becomes a problem. 33 | require "active_support/all" 34 | 35 | require_relative "authlogic/controller_adapters/rails_adapter" if defined?(Rails) 36 | require_relative "authlogic/controller_adapters/sinatra_adapter" if defined?(Sinatra) 37 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # Provides the base functionality for acts_as_authentic 6 | module Base 7 | def self.included(klass) 8 | klass.class_eval do 9 | class_attribute :acts_as_authentic_modules 10 | self.acts_as_authentic_modules ||= [] 11 | extend Authlogic::Config 12 | extend Config 13 | end 14 | end 15 | 16 | # The primary configuration of a model (often, `User`) for use with 17 | # authlogic. These methods become class methods of ::ActiveRecord::Base. 18 | module Config 19 | # This includes a lot of helpful methods for authenticating records 20 | # which the Authlogic::Session module relies on. To use it just do: 21 | # 22 | # class User < ApplicationRecord 23 | # acts_as_authentic 24 | # end 25 | # 26 | # Configuration is easy: 27 | # 28 | # acts_as_authentic do |c| 29 | # c.my_configuration_option = my_value 30 | # end 31 | # 32 | # See the various sub modules for the configuration they provide. 33 | def acts_as_authentic 34 | yield self if block_given? 35 | return unless db_setup? 36 | acts_as_authentic_modules.each { |mod| include mod } 37 | end 38 | 39 | # Since this part of Authlogic deals with another class, ActiveRecord, 40 | # we can't just start including things in ActiveRecord itself. A lot of 41 | # these module includes need to be triggered by the acts_as_authentic 42 | # method call. For example, you don't want to start adding in email 43 | # validations and what not into a model that has nothing to do with 44 | # Authlogic. 45 | # 46 | # That being said, this is your tool for extending Authlogic and 47 | # "hooking" into the acts_as_authentic call. 48 | def add_acts_as_authentic_module(mod, action = :append) 49 | modules = acts_as_authentic_modules.clone 50 | case action 51 | when :append 52 | modules << mod 53 | when :prepend 54 | modules = [mod] + modules 55 | end 56 | modules.uniq! 57 | self.acts_as_authentic_modules = modules 58 | end 59 | 60 | # This is the same as add_acts_as_authentic_module, except that it 61 | # removes the module from the list. 62 | def remove_acts_as_authentic_module(mod) 63 | modules = acts_as_authentic_modules.clone 64 | modules.delete(mod) 65 | self.acts_as_authentic_modules = modules 66 | end 67 | 68 | # Some Authlogic modules requires a database connection with a existing 69 | # users table by the moment when you call the `acts_as_authentic` 70 | # method. If you try to call `acts_as_authentic` without a database 71 | # connection, it will raise a `Authlogic::ModelSetupError`. 72 | # 73 | # If you rely on the User model before the database is setup correctly, 74 | # set this field to false. 75 | # * Default: false 76 | # * Accepts: Boolean 77 | def raise_on_model_setup_error(value = nil) 78 | rw_config(:raise_on_model_setup_error, value, false) 79 | end 80 | alias raise_on_model_setup_error= raise_on_model_setup_error 81 | 82 | private 83 | 84 | def db_setup? 85 | column_names 86 | true 87 | rescue StandardError 88 | raise ModelSetupError if raise_on_model_setup_error 89 | false 90 | end 91 | 92 | def first_column_to_exist(*columns_to_check) 93 | if db_setup? 94 | columns_to_check.each do |column_name| 95 | if column_names.include?(column_name.to_s) 96 | return column_name.to_sym 97 | end 98 | end 99 | end 100 | columns_to_check.first&.to_sym 101 | end 102 | end 103 | end 104 | end 105 | end 106 | 107 | ActiveSupport.on_load :active_record do 108 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Base 109 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Email 110 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::LoggedInStatus 111 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Login 112 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::MagicColumns 113 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::Password 114 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PerishableToken 115 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::PersistenceToken 116 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SessionMaintenance 117 | ActiveRecord::Base.send :include, Authlogic::ActsAsAuthentic::SingleAccessToken 118 | end 119 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/email.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # Sometimes models won't have an explicit "login" or "username" field. 6 | # Instead they want to use the email field. In this case, authlogic provides 7 | # validations to make sure the email submited is actually a valid email. 8 | # Don't worry, if you do have a login or username field, Authlogic will 9 | # still validate your email field. One less thing you have to worry about. 10 | module Email 11 | def self.included(klass) 12 | klass.class_eval do 13 | extend Config 14 | end 15 | end 16 | 17 | # Configuration to modify how Authlogic handles the email field. 18 | module Config 19 | # The name of the field that stores email addresses. 20 | # 21 | # * Default: :email, if it exists 22 | # * Accepts: Symbol 23 | def email_field(value = nil) 24 | rw_config(:email_field, value, first_column_to_exist(nil, :email, :email_address)) 25 | end 26 | alias email_field= email_field 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/logged_in_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # Since web applications are stateless there is not sure fire way to tell if 6 | # a user is logged in or not, from the database perspective. The best way to 7 | # do this is to provide a "timeout" based on inactivity. So if that user is 8 | # inactive for a certain amount of time we assume they are logged out. 9 | # That's what this module is all about. 10 | module LoggedInStatus 11 | def self.included(klass) 12 | klass.class_eval do 13 | extend Config 14 | add_acts_as_authentic_module(Methods) 15 | end 16 | end 17 | 18 | # All configuration for the logged in status feature set. 19 | module Config 20 | # The timeout to determine when a user is logged in or not. 21 | # 22 | # * Default: 10.minutes 23 | # * Accepts: Fixnum 24 | def logged_in_timeout(value = nil) 25 | rw_config(:logged_in_timeout, (!value.nil? && value.to_i) || value, 10.minutes.to_i) 26 | end 27 | alias logged_in_timeout= logged_in_timeout 28 | end 29 | 30 | # All methods for the logged in status feature seat. 31 | module Methods 32 | def self.included(klass) 33 | return unless klass.column_names.include?("last_request_at") 34 | 35 | klass.class_eval do 36 | include InstanceMethods 37 | scope( 38 | :logged_in, 39 | lambda do 40 | where( 41 | "last_request_at > ? and current_login_at IS NOT NULL", 42 | logged_in_timeout.seconds.ago 43 | ) 44 | end 45 | ) 46 | scope( 47 | :logged_out, 48 | lambda do 49 | where( 50 | "last_request_at is NULL or last_request_at <= ?", 51 | logged_in_timeout.seconds.ago 52 | ) 53 | end 54 | ) 55 | end 56 | end 57 | 58 | # :nodoc: 59 | module InstanceMethods 60 | # Returns true if the last_request_at > logged_in_timeout. 61 | def logged_in? 62 | unless respond_to?(:last_request_at) 63 | raise( 64 | "Can not determine the records login state because " \ 65 | "there is no last_request_at column" 66 | ) 67 | end 68 | !last_request_at.nil? && last_request_at > logged_in_timeout.seconds.ago 69 | end 70 | 71 | # Opposite of logged_in? 72 | def logged_out? 73 | !logged_in? 74 | end 75 | 76 | private 77 | 78 | def logged_in_timeout 79 | self.class.logged_in_timeout 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "queries/case_sensitivity" 4 | require_relative "queries/find_with_case" 5 | 6 | module Authlogic 7 | module ActsAsAuthentic 8 | # Handles everything related to the login field. 9 | module Login 10 | def self.included(klass) 11 | klass.class_eval do 12 | extend Config 13 | end 14 | end 15 | 16 | # Configuration for the login field. 17 | module Config 18 | # The name of the login field in the database. 19 | # 20 | # * Default: :login or :username, if they exist 21 | # * Accepts: Symbol 22 | def login_field(value = nil) 23 | rw_config(:login_field, value, first_column_to_exist(nil, :login, :username)) 24 | end 25 | alias login_field= login_field 26 | 27 | # This method allows you to find a record with the given login. If you 28 | # notice, with Active Record you have the UniquenessValidator class. 29 | # They give you a :case_sensitive option. I handle this in the same 30 | # manner that they handle that. If you are using the login field, set 31 | # false for the :case_sensitive option in 32 | # validates_uniqueness_of_login_field_options and the column doesn't 33 | # have a case-insensitive collation, this method will modify the query 34 | # to look something like: 35 | # 36 | # "LOWER(#{quoted_table_name}.#{login_field}) = LOWER(#{login})" 37 | # 38 | # If you don't specify this it just uses a regular case-sensitive search 39 | # (with the binary modifier if necessary): 40 | # 41 | # "BINARY #{login_field} = #{login}" 42 | # 43 | # The above also applies for using email as your login, except that you 44 | # need to set the :case_sensitive in 45 | # validates_uniqueness_of_email_field_options to false. 46 | # 47 | # @api public 48 | def find_by_smart_case_login_field(login) 49 | field = login_field || email_field 50 | sensitive = Queries::CaseSensitivity.new(self, field).sensitive? 51 | find_with_case(field, login, sensitive) 52 | end 53 | 54 | private 55 | 56 | # @api private 57 | def find_with_case(field, value, sensitive) 58 | Queries::FindWithCase.new(self, field, value, sensitive).execute 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/magic_columns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # Magic columns are like ActiveRecord's created_at and updated_at columns. 6 | # They are "magically" maintained for you. Authlogic has the same thing, but 7 | # these are maintained on the session side. Please see "Magic Columns" in 8 | # `Session::Base` for more details. This module merely adds validations for 9 | # the magic columns if they exist. 10 | module MagicColumns 11 | def self.included(klass) 12 | klass.class_eval do 13 | add_acts_as_authentic_module(Methods) 14 | end 15 | end 16 | 17 | # Methods relating to the magic columns 18 | module Methods 19 | def self.included(klass) 20 | klass.class_eval do 21 | if column_names.include?("login_count") 22 | validates_numericality_of :login_count, 23 | only_integer: true, 24 | greater_than_or_equal_to: 0, 25 | allow_nil: true 26 | end 27 | if column_names.include?("failed_login_count") 28 | validates_numericality_of :failed_login_count, 29 | only_integer: true, 30 | greater_than_or_equal_to: 0, 31 | allow_nil: true 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/perishable_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # This provides a handy token that is "perishable", meaning the token is 6 | # only good for a certain amount of time. 7 | # 8 | # This is useful for resetting password, confirming accounts, etc. Typically 9 | # during these actions you send them this token in an email. Once they use 10 | # the token and do what they need to do, that token should expire. 11 | # 12 | # Don't worry about maintaining the token, changing it, or expiring it 13 | # yourself. Authlogic does all of this for you. See the sub modules for all 14 | # of the tools Authlogic provides to you. 15 | module PerishableToken 16 | def self.included(klass) 17 | klass.class_eval do 18 | extend Config 19 | add_acts_as_authentic_module(Methods) 20 | end 21 | end 22 | 23 | # Configure the perishable token. 24 | module Config 25 | # When using the find_using_perishable_token method the token can 26 | # expire. If the token is expired, no record will be returned. Use this 27 | # option to specify how long the token is valid for. 28 | # 29 | # * Default: 10.minutes 30 | # * Accepts: Fixnum 31 | def perishable_token_valid_for(value = nil) 32 | rw_config( 33 | :perishable_token_valid_for, 34 | (!value.nil? && value.to_i) || value, 35 | 10.minutes.to_i 36 | ) 37 | end 38 | alias perishable_token_valid_for= perishable_token_valid_for 39 | 40 | # Authlogic tries to expire and change the perishable token as much as 41 | # possible, without compromising its purpose. If you want to manage it 42 | # yourself, set this to true. 43 | # 44 | # * Default: false 45 | # * Accepts: Boolean 46 | def disable_perishable_token_maintenance(value = nil) 47 | rw_config(:disable_perishable_token_maintenance, value, false) 48 | end 49 | alias disable_perishable_token_maintenance= disable_perishable_token_maintenance 50 | end 51 | 52 | # All methods relating to the perishable token. 53 | module Methods 54 | def self.included(klass) 55 | return unless klass.column_names.include?("perishable_token") 56 | 57 | klass.class_eval do 58 | extend ClassMethods 59 | include InstanceMethods 60 | 61 | validates_uniqueness_of :perishable_token, case_sensitive: true, 62 | if: :will_save_change_to_perishable_token? 63 | before_save :reset_perishable_token, unless: :disable_perishable_token_maintenance? 64 | end 65 | end 66 | 67 | # :nodoc: 68 | module ClassMethods 69 | # Use this method to find a record with a perishable token. This 70 | # method does 2 things for you: 71 | # 72 | # 1. It ignores blank tokens 73 | # 2. It enforces the perishable_token_valid_for configuration option. 74 | # 75 | # If you want to use a different timeout value, just pass it as the 76 | # second parameter: 77 | # 78 | # User.find_using_perishable_token(token, 1.hour) 79 | def find_using_perishable_token(token, age = perishable_token_valid_for) 80 | return if token.blank? 81 | age = age.to_i 82 | 83 | conditions_sql = "perishable_token = ?" 84 | conditions_subs = [token.to_s] 85 | 86 | if column_names.include?("updated_at") && age > 0 87 | conditions_sql += " and updated_at > ?" 88 | conditions_subs << age.seconds.ago 89 | end 90 | 91 | where(conditions_sql, *conditions_subs).first 92 | end 93 | 94 | # This method will raise ActiveRecord::NotFound if no record is found. 95 | def find_using_perishable_token!(token, age = perishable_token_valid_for) 96 | find_using_perishable_token(token, age) || raise(ActiveRecord::RecordNotFound) 97 | end 98 | end 99 | 100 | # :nodoc: 101 | module InstanceMethods 102 | # Resets the perishable token to a random friendly token. 103 | def reset_perishable_token 104 | self.perishable_token = Random.friendly_token 105 | end 106 | 107 | # Same as reset_perishable_token, but then saves the record afterwards. 108 | def reset_perishable_token! 109 | reset_perishable_token 110 | save_without_session_maintenance(validate: false) 111 | end 112 | 113 | # A convenience method based on the 114 | # disable_perishable_token_maintenance configuration option. 115 | def disable_perishable_token_maintenance? 116 | self.class.disable_perishable_token_maintenance == true 117 | end 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/persistence_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # Maintains the persistence token, the token responsible for persisting sessions. This token 6 | # gets stored in the session and the cookie. 7 | module PersistenceToken 8 | def self.included(klass) 9 | klass.class_eval do 10 | add_acts_as_authentic_module(Methods) 11 | end 12 | end 13 | 14 | # Methods for the persistence token. 15 | module Methods 16 | def self.included(klass) 17 | klass.class_eval do 18 | extend ClassMethods 19 | include InstanceMethods 20 | 21 | # If the table does not have a password column, then 22 | # `after_password_set` etc. will not be defined. See 23 | # `Authlogic::ActsAsAuthentic::Password::Callbacks.included` 24 | if respond_to?(:after_password_set) && respond_to?(:after_password_verification) 25 | after_password_set :reset_persistence_token 26 | after_password_verification :reset_persistence_token!, if: :reset_persistence_token? 27 | end 28 | 29 | validates_presence_of :persistence_token 30 | validates_uniqueness_of :persistence_token, case_sensitive: true, 31 | if: :will_save_change_to_persistence_token? 32 | 33 | before_validation :reset_persistence_token, if: :reset_persistence_token? 34 | end 35 | end 36 | 37 | # :nodoc: 38 | module ClassMethods 39 | # Resets ALL persistence tokens in the database, which will require 40 | # all users to re-authenticate. 41 | def forget_all 42 | # Paginate these to save on memory 43 | find_each(batch_size: 50, &:forget!) 44 | end 45 | end 46 | 47 | # :nodoc: 48 | module InstanceMethods 49 | # Resets the persistence_token field to a random hex value. 50 | def reset_persistence_token 51 | self.persistence_token = Authlogic::Random.hex_token 52 | end 53 | 54 | # Same as reset_persistence_token, but then saves the record. 55 | def reset_persistence_token! 56 | reset_persistence_token 57 | save_without_session_maintenance(validate: false) 58 | end 59 | alias forget! reset_persistence_token! 60 | 61 | private 62 | 63 | def reset_persistence_token? 64 | persistence_token.blank? 65 | end 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/queries/case_sensitivity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | module Queries 6 | # @api private 7 | class CaseSensitivity 8 | E_UNABLE_TO_DETERMINE_SENSITIVITY = <<~EOS 9 | Authlogic was unable to determine what case-sensitivity to use when 10 | searching for email/login. To specify a sensitivity, validate the 11 | uniqueness of the email/login and use the `case_sensitive` option, 12 | like this: 13 | 14 | validates :email, uniqueness: { case_sensitive: false } 15 | 16 | Authlogic will now perform a case-insensitive query. 17 | EOS 18 | 19 | # @api private 20 | def initialize(model_class, attribute) 21 | @model_class = model_class 22 | @attribute = attribute.to_sym 23 | end 24 | 25 | # @api private 26 | def sensitive? 27 | sensitive = uniqueness_validator_options[:case_sensitive] 28 | if sensitive.nil? 29 | ::Kernel.warn(E_UNABLE_TO_DETERMINE_SENSITIVITY) 30 | false 31 | else 32 | sensitive 33 | end 34 | end 35 | 36 | private 37 | 38 | # @api private 39 | def uniqueness_validator 40 | @model_class.validators.select { |v| 41 | v.is_a?(::ActiveRecord::Validations::UniquenessValidator) && 42 | v.attributes == [@attribute] 43 | }.first 44 | end 45 | 46 | # @api private 47 | def uniqueness_validator_options 48 | uniqueness_validator&.options || {} 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/queries/find_with_case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | module Queries 6 | # The query used by public-API method `find_by_smart_case_login_field`. 7 | # 8 | # We use the rails methods `case_insensitive_comparison` and 9 | # `case_sensitive_comparison`. These methods nicely take into account 10 | # MySQL collations. (Consider the case where a user *says* they want a 11 | # case-sensitive uniqueness validation, but then they configure their 12 | # database to have an insensitive collation. Rails will handle this for 13 | # us, by downcasing, see 14 | # `active_record/connection_adapters/abstract_mysql_adapter.rb`) So that's 15 | # great! But, these methods are not part of rails' public API, so there 16 | # are no docs. So, everything we know about how to use the methods 17 | # correctly comes from mimicing what we find in 18 | # `active_record/validations/uniqueness.rb`. 19 | # 20 | # @api private 21 | class FindWithCase 22 | # Dup ActiveRecord.gem_version before freezing, in case someone 23 | # else wants to modify it. Freezing modifies an object in place. 24 | # https://github.com/binarylogic/authlogic/pull/590 25 | AR_GEM_VERSION = ::ActiveRecord.gem_version.dup.freeze 26 | 27 | # @api private 28 | def initialize(model_class, field, value, sensitive) 29 | @model_class = model_class 30 | @field = field.to_s 31 | @value = value 32 | @sensitive = sensitive 33 | end 34 | 35 | # @api private 36 | def execute 37 | @model_class.where(comparison).first 38 | end 39 | 40 | private 41 | 42 | # @api private 43 | # @return Arel::Nodes::Equality 44 | def comparison 45 | @sensitive ? sensitive_comparison : insensitive_comparison 46 | end 47 | 48 | # @api private 49 | def insensitive_comparison 50 | if AR_GEM_VERSION > Gem::Version.new("5.3") 51 | @model_class.connection.case_insensitive_comparison( 52 | @model_class.arel_table[@field], @value 53 | ) 54 | else 55 | @model_class.connection.case_insensitive_comparison( 56 | @model_class.arel_table, 57 | @field, 58 | @model_class.columns_hash[@field], 59 | @value 60 | ) 61 | end 62 | end 63 | 64 | # @api private 65 | def sensitive_comparison 66 | bound_value = @model_class.predicate_builder.build_bind_attribute(@field, @value) 67 | if AR_GEM_VERSION > Gem::Version.new("5.3") 68 | @model_class.connection.case_sensitive_comparison( 69 | @model_class.arel_table[@field], bound_value 70 | ) 71 | else 72 | @model_class.connection.case_sensitive_comparison( 73 | @model_class.arel_table, 74 | @field, 75 | @model_class.columns_hash[@field], 76 | bound_value 77 | ) 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/authlogic/acts_as_authentic/single_access_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ActsAsAuthentic 5 | # This module is responsible for maintaining the single_access token. For 6 | # more information the single access token and how to use it, see "Params" 7 | # in `Session::Base`. 8 | module SingleAccessToken 9 | def self.included(klass) 10 | klass.class_eval do 11 | extend Config 12 | add_acts_as_authentic_module(Methods) 13 | end 14 | end 15 | 16 | # All configuration for the single_access token aspect of acts_as_authentic. 17 | # 18 | # These methods become class methods of ::ActiveRecord::Base. 19 | module Config 20 | # The single access token is used for authentication via URLs, such as a private 21 | # feed. That being said, if the user changes their password, that token probably 22 | # shouldn't change. If it did, the user would have to update all of their URLs. So 23 | # be default this is option is disabled, if you need it, feel free to turn it on. 24 | # 25 | # * Default: false 26 | # * Accepts: Boolean 27 | def change_single_access_token_with_password(value = nil) 28 | rw_config(:change_single_access_token_with_password, value, false) 29 | end 30 | alias change_single_access_token_with_password= change_single_access_token_with_password 31 | end 32 | 33 | # All method, for the single_access token aspect of acts_as_authentic. 34 | # 35 | # This module, as one of the `acts_as_authentic_modules`, is only included 36 | # into an ActiveRecord model if that model calls `acts_as_authentic`. 37 | module Methods 38 | def self.included(klass) 39 | return unless klass.column_names.include?("single_access_token") 40 | 41 | klass.class_eval do 42 | include InstanceMethods 43 | validates_uniqueness_of :single_access_token, 44 | case_sensitive: true, 45 | if: :will_save_change_to_single_access_token? 46 | 47 | before_validation :reset_single_access_token, if: :reset_single_access_token? 48 | if respond_to?(:after_password_set) 49 | after_password_set( 50 | :reset_single_access_token, 51 | if: :change_single_access_token_with_password? 52 | ) 53 | end 54 | end 55 | end 56 | 57 | # :nodoc: 58 | module InstanceMethods 59 | # Resets the single_access_token to a random friendly token. 60 | def reset_single_access_token 61 | self.single_access_token = Authlogic::Random.friendly_token 62 | end 63 | 64 | # same as reset_single_access_token, but then saves the record. 65 | def reset_single_access_token! 66 | reset_single_access_token 67 | save_without_session_maintenance 68 | end 69 | 70 | protected 71 | 72 | def reset_single_access_token? 73 | single_access_token.blank? 74 | end 75 | 76 | def change_single_access_token_with_password? 77 | self.class.change_single_access_token_with_password == true 78 | end 79 | end 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/authlogic/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | # Mixed into `Authlogic::ActsAsAuthentic::Base` and 5 | # `Authlogic::Session::Base`. 6 | module Config 7 | E_USE_NORMAL_RAILS_VALIDATION = <<~EOS 8 | This Authlogic configuration option (%s) is deprecated. Use normal 9 | ActiveRecord validation instead. Detailed instructions: 10 | https://github.com/binarylogic/authlogic/blob/master/doc/use_normal_rails_validation.md 11 | EOS 12 | 13 | def self.extended(klass) 14 | klass.class_eval do 15 | # TODO: Is this a confusing name, given this module is mixed into 16 | # both `Authlogic::ActsAsAuthentic::Base` and 17 | # `Authlogic::Session::Base`? Perhaps a more generic name, like 18 | # `authlogic_config` would be better? 19 | class_attribute :acts_as_authentic_config 20 | self.acts_as_authentic_config ||= {} 21 | end 22 | end 23 | 24 | private 25 | 26 | def deprecate_authlogic_config(method_name) 27 | ::ActiveSupport::Deprecation.new.warn( 28 | format(E_USE_NORMAL_RAILS_VALIDATION, method_name) 29 | ) 30 | end 31 | 32 | # This is a one-liner method to write a config setting, read the config 33 | # setting, and also set a default value for the setting. 34 | def rw_config(key, value, default_value = nil) 35 | if value.nil? 36 | acts_as_authentic_config.include?(key) ? acts_as_authentic_config[key] : default_value 37 | else 38 | self.acts_as_authentic_config = acts_as_authentic_config.merge(key => value) 39 | value 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/authlogic/controller_adapters/abstract_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ControllerAdapters # :nodoc: 5 | # Allows you to use Authlogic in any framework you want, not just rails. See 6 | # the RailsAdapter for an example of how to adapt Authlogic to work with 7 | # your framework. 8 | class AbstractAdapter 9 | E_COOKIE_DOMAIN_ADAPTER = "The cookie_domain method has not been " \ 10 | "implemented by the controller adapter" 11 | ENV_SESSION_OPTIONS = "rack.session.options" 12 | 13 | attr_accessor :controller 14 | 15 | def initialize(controller) 16 | self.controller = controller 17 | end 18 | 19 | def authenticate_with_http_basic 20 | @auth = Rack::Auth::Basic::Request.new(controller.request.env) 21 | if @auth.provided? && @auth.basic? 22 | yield(*@auth.credentials) 23 | else 24 | false 25 | end 26 | end 27 | 28 | def cookies 29 | controller.cookies 30 | end 31 | 32 | def cookie_domain 33 | raise NotImplementedError, E_COOKIE_DOMAIN_ADAPTER 34 | end 35 | 36 | def params 37 | controller.params 38 | end 39 | 40 | def request 41 | controller.request 42 | end 43 | 44 | def request_content_type 45 | request.content_type 46 | end 47 | 48 | # Inform Rack that we would like a new session ID to be assigned. Changes 49 | # the ID, but not the contents of the session. 50 | # 51 | # The `:renew` option is read by `rack/session/abstract/id.rb`. 52 | # 53 | # This is how Devise (via warden) implements defense against Session 54 | # Fixation. Our implementation is copied directly from the warden gem 55 | # (set_user in warden/proxy.rb) 56 | def renew_session_id 57 | env = request.env 58 | options = env[ENV_SESSION_OPTIONS] 59 | if options 60 | if options.frozen? 61 | env[ENV_SESSION_OPTIONS] = options.merge(renew: true).freeze 62 | else 63 | options[:renew] = true 64 | end 65 | end 66 | end 67 | 68 | def session 69 | controller.session 70 | end 71 | 72 | def responds_to_single_access_allowed? 73 | controller.respond_to?(:single_access_allowed?, true) 74 | end 75 | 76 | def single_access_allowed? 77 | controller.send(:single_access_allowed?) 78 | end 79 | 80 | # You can disable the updating of `last_request_at` 81 | # on a per-controller basis. 82 | # 83 | # # in your controller 84 | # def last_request_update_allowed? 85 | # false 86 | # end 87 | # 88 | # For example, what if you had a javascript function that polled the 89 | # server updating how much time is left in their session before it 90 | # times out. Obviously you would want to ignore this request, because 91 | # then the user would never time out. So you can do something like 92 | # this in your controller: 93 | # 94 | # def last_request_update_allowed? 95 | # action_name != "update_session_time_left" 96 | # end 97 | # 98 | # See `authlogic/session/magic_columns.rb` to learn more about the 99 | # `last_request_at` column itself. 100 | def last_request_update_allowed? 101 | if controller.respond_to?(:last_request_update_allowed?, true) 102 | controller.send(:last_request_update_allowed?) 103 | else 104 | true 105 | end 106 | end 107 | 108 | def respond_to_missing?(*args) 109 | super(*args) || controller.respond_to?(*args) 110 | end 111 | 112 | private 113 | 114 | def method_missing(id, *args, &block) 115 | controller.send(id, *args, &block) 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/authlogic/controller_adapters/rack_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ControllerAdapters 5 | # Adapter for authlogic to make it function as a Rack middleware. 6 | # First you'll have write your own Rack adapter where you have to set your cookie domain. 7 | # 8 | # class YourRackAdapter < Authlogic::ControllerAdapters::RackAdapter 9 | # def cookie_domain 10 | # 'your_cookie_domain_here.com' 11 | # end 12 | # end 13 | # 14 | # Next you need to set up a rack middleware like this: 15 | # 16 | # class AuthlogicMiddleware 17 | # def initialize(app) 18 | # @app = app 19 | # end 20 | # 21 | # def call(env) 22 | # YourRackAdapter.new(env) 23 | # @app.call(env) 24 | # end 25 | # end 26 | # 27 | # And that is all! Now just load this middleware into rack: 28 | # 29 | # use AuthlogicMiddleware 30 | # 31 | # Authlogic will expect a User and a UserSession object to be present: 32 | # 33 | # class UserSession < Authlogic::Session::Base 34 | # # Authlogic options go here 35 | # end 36 | # 37 | # class User < ApplicationRecord 38 | # acts_as_authentic 39 | # end 40 | # 41 | class RackAdapter < AbstractAdapter 42 | def initialize(env) 43 | # We use the Rack::Request object as the controller object. 44 | # For this to work, we have to add some glue. 45 | request = Rack::Request.new(env) 46 | 47 | request.instance_eval do 48 | def request 49 | self 50 | end 51 | 52 | def remote_ip 53 | ip 54 | end 55 | end 56 | 57 | super(request) 58 | Authlogic::Session::Base.controller = self 59 | end 60 | 61 | # Rack Requests stores cookies with not just the value, but also with 62 | # flags and expire information in the hash. Authlogic does not like this, 63 | # so we drop everything except the cookie value. 64 | def cookies 65 | controller 66 | .cookies 67 | .map { |key, value_hash| { key => value_hash[:value] } } 68 | .inject(:merge) || {} 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/authlogic/controller_adapters/rails_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module ControllerAdapters 5 | # Adapts authlogic to work with rails. The point is to close the gap between 6 | # what authlogic expects and what the rails controller object provides. 7 | # Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, 8 | # etc. 9 | class RailsAdapter < AbstractAdapter 10 | def authenticate_with_http_basic(&block) 11 | controller.authenticate_with_http_basic(&block) 12 | end 13 | 14 | # Returns a `ActionDispatch::Cookies::CookieJar`. See the AC guide 15 | # http://guides.rubyonrails.org/action_controller_overview.html#cookies 16 | def cookies 17 | controller.respond_to?(:cookies, true) ? controller.send(:cookies) : nil 18 | end 19 | 20 | def cookie_domain 21 | controller.request.session_options[:domain] 22 | end 23 | 24 | def request_content_type 25 | request.format.to_s 26 | end 27 | 28 | # Lets Authlogic know about the controller object via a before filter, AKA 29 | # "activates" authlogic. 30 | module RailsImplementation 31 | def self.included(klass) # :nodoc: 32 | klass.prepend_before_action :activate_authlogic 33 | end 34 | 35 | private 36 | 37 | def activate_authlogic 38 | Authlogic::Session::Base.controller = RailsAdapter.new(self) 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | ActiveSupport.on_load(:action_controller) do 46 | include Authlogic::ControllerAdapters::RailsAdapter::RailsImplementation 47 | end 48 | -------------------------------------------------------------------------------- /lib/authlogic/controller_adapters/sinatra_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Authlogic bridge for Sinatra 4 | module Authlogic 5 | module ControllerAdapters 6 | module SinatraAdapter 7 | # Cookie management functions 8 | class Cookies 9 | attr_reader :request, :response 10 | 11 | def initialize(request, response) 12 | @request = request 13 | @response = response 14 | end 15 | 16 | def delete(key, options = {}) 17 | @response.delete_cookie(key, options) 18 | end 19 | 20 | def []=(key, options) 21 | @response.set_cookie(key, options) 22 | end 23 | 24 | def method_missing(meth, *args, &block) 25 | @request.cookies.send(meth, *args, &block) 26 | end 27 | end 28 | 29 | # Thin wrapper around request and response. 30 | class Controller 31 | attr_reader :request, :response, :cookies 32 | 33 | def initialize(request, response) 34 | @request = request 35 | @cookies = Cookies.new(request, response) 36 | end 37 | 38 | def session 39 | env["rack.session"] 40 | end 41 | 42 | def method_missing(meth, *args, &block) 43 | @request.send meth, *args, &block 44 | end 45 | end 46 | 47 | # Sinatra controller adapter 48 | class Adapter < AbstractAdapter 49 | def cookie_domain 50 | env["SERVER_NAME"] 51 | end 52 | 53 | # Mixed into `Sinatra::Base` 54 | module Implementation 55 | def self.included(klass) 56 | klass.send :before do 57 | controller = Controller.new(request, response) 58 | Authlogic::Session::Base.controller = Adapter.new(controller) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | 67 | Sinatra::Base.send(:include, Authlogic::ControllerAdapters::SinatraAdapter::Adapter::Implementation) 68 | -------------------------------------------------------------------------------- /lib/authlogic/cookie_credentials.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | # Represents the credentials *in* the cookie. The value of the cookie. 5 | # This is primarily a data object. It doesn't interact with controllers. 6 | # It doesn't know about eg. cookie expiration. 7 | # 8 | # @api private 9 | class CookieCredentials 10 | # @api private 11 | class ParseError < RuntimeError 12 | end 13 | 14 | DELIMITER = "::" 15 | 16 | attr_reader :persistence_token, :record_id, :remember_me_until 17 | 18 | # @api private 19 | # @param persistence_token [String] 20 | # @param record_id [String, Numeric] 21 | # @param remember_me_until [ActiveSupport::TimeWithZone] 22 | def initialize(persistence_token, record_id, remember_me_until) 23 | @persistence_token = persistence_token 24 | @record_id = record_id 25 | @remember_me_until = remember_me_until 26 | end 27 | 28 | class << self 29 | # @api private 30 | def parse(string) 31 | parts = string.split(DELIMITER) 32 | unless (1..3).cover?(parts.length) 33 | raise ParseError, format("Expected 1..3 parts, got %d", parts.length) 34 | end 35 | new(parts[0], parts[1], parse_time(parts[2])) 36 | end 37 | 38 | private 39 | 40 | # @api private 41 | def parse_time(string) 42 | return if string.nil? 43 | ::Time.parse(string) 44 | rescue ::ArgumentError => e 45 | raise ParseError, format("Found cookie, cannot parse remember_me_until: #{e}") 46 | end 47 | end 48 | 49 | # @api private 50 | def remember_me? 51 | !@remember_me_until.nil? 52 | end 53 | 54 | # @api private 55 | def to_s 56 | [ 57 | @persistence_token, 58 | @record_id.to_s, 59 | @remember_me_until&.iso8601 60 | ].compact.join(DELIMITER) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | # The acts_as_authentic method has a crypto_provider option. This allows you 5 | # to use any type of encryption you like. Just create a class with a class 6 | # level encrypt and matches? method. See example below. 7 | # 8 | # === Example 9 | # 10 | # class MyAwesomeEncryptionMethod 11 | # def self.encrypt(*tokens) 12 | # # The tokens passed will be an array of objects, what type of object 13 | # # is irrelevant, just do what you need to do with them and return a 14 | # # single encrypted string. For example, you will most likely join all 15 | # # of the objects into a single string and then encrypt that string. 16 | # end 17 | # 18 | # def self.matches?(crypted, *tokens) 19 | # # Return true if the crypted string matches the tokens. Depending on 20 | # # your algorithm you might decrypt the string then compare it to the 21 | # # token, or you might encrypt the tokens and make sure it matches the 22 | # # crypted string, its up to you. 23 | # end 24 | # end 25 | module CryptoProviders 26 | autoload :MD5, "authlogic/crypto_providers/md5" 27 | autoload :Sha1, "authlogic/crypto_providers/sha1" 28 | autoload :Sha256, "authlogic/crypto_providers/sha256" 29 | autoload :Sha512, "authlogic/crypto_providers/sha512" 30 | autoload :BCrypt, "authlogic/crypto_providers/bcrypt" 31 | autoload :SCrypt, "authlogic/crypto_providers/scrypt" 32 | 33 | # Guide users to choose a better crypto provider. 34 | class Guidance 35 | BUILTIN_PROVIDER_PREFIX = "Authlogic::CryptoProviders::" 36 | NONADAPTIVE_ALGORITHM = <<~EOS 37 | You have selected %s as your authlogic crypto provider. This algorithm 38 | does not have any practical known attacks against it. However, there are 39 | better choices. 40 | 41 | Authlogic has no plans yet to deprecate this crypto provider. However, 42 | we recommend transitioning to a more secure, adaptive hashing algorithm, 43 | like scrypt. Adaptive algorithms are designed to slow down brute force 44 | attacks, and over time the iteration count can be increased to make it 45 | slower, so it remains resistant to brute-force search attacks even in 46 | the face of increasing computation power. 47 | 48 | Use the transition_from_crypto_providers option to make the transition 49 | painless for your users. 50 | EOS 51 | VULNERABLE_ALGORITHM = <<~EOS 52 | You have selected %s as your authlogic crypto provider. It is a poor 53 | choice because there are known attacks against this algorithm. 54 | 55 | Authlogic has no plans yet to deprecate this crypto provider. However, 56 | we recommend transitioning to a secure hashing algorithm. We recommend 57 | an adaptive algorithm, like scrypt. 58 | 59 | Use the transition_from_crypto_providers option to make the transition 60 | painless for your users. 61 | EOS 62 | 63 | def initialize(provider) 64 | @provider = provider 65 | end 66 | 67 | def impart_wisdom 68 | return unless @provider.is_a?(Class) 69 | 70 | # We can only impart wisdom about our own built-in providers. 71 | absolute_name = @provider.name 72 | return unless absolute_name.start_with?(BUILTIN_PROVIDER_PREFIX) 73 | 74 | # Inspect the string name of the provider, rather than using the 75 | # constants in our `when` clauses. If we used the constants, we'd 76 | # negate the benefits of the `autoload` above. 77 | name = absolute_name.demodulize 78 | case name 79 | when "MD5", "Sha1" 80 | warn(format(VULNERABLE_ALGORITHM, name)) 81 | when "Sha256", "Sha512" 82 | warn(format(NONADAPTIVE_ALGORITHM, name)) 83 | end 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/bcrypt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bcrypt" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | # The family of adaptive hash functions (BCrypt, SCrypt, PBKDF2) 8 | # is the best choice for password storage today. They have the 9 | # three properties of password hashing that are desirable. They 10 | # are one-way, unique, and slow. While a salted SHA or MD5 hash is 11 | # one-way and unique, preventing rainbow table attacks, they are 12 | # still lightning fast and attacks on the stored passwords are 13 | # much more effective. This benchmark demonstrates the effective 14 | # slowdown that BCrypt provides: 15 | # 16 | # require "bcrypt" 17 | # require "digest" 18 | # require "benchmark" 19 | # 20 | # Benchmark.bm(18) do |x| 21 | # x.report("BCrypt (cost = 10:") { 22 | # 100.times { BCrypt::Password.create("mypass", :cost => 10) } 23 | # } 24 | # x.report("BCrypt (cost = 4:") { 25 | # 100.times { BCrypt::Password.create("mypass", :cost => 4) } 26 | # } 27 | # x.report("Sha512:") { 28 | # 100.times { Digest::SHA512.hexdigest("mypass") } 29 | # } 30 | # x.report("Sha1:") { 31 | # 100.times { Digest::SHA1.hexdigest("mypass") } 32 | # } 33 | # end 34 | # 35 | # user system total real 36 | # BCrypt (cost = 10): 37.360000 0.020000 37.380000 ( 37.558943) 37 | # BCrypt (cost = 4): 0.680000 0.000000 0.680000 ( 0.677460) 38 | # Sha512: 0.000000 0.000000 0.000000 ( 0.000672) 39 | # Sha1: 0.000000 0.000000 0.000000 ( 0.000454) 40 | # 41 | # You can play around with the cost to get that perfect balance 42 | # between performance and security. A default cost of 10 is the 43 | # best place to start. 44 | # 45 | # Decided BCrypt is for you? Just install the bcrypt gem: 46 | # 47 | # gem install bcrypt 48 | # 49 | # Tell acts_as_authentic to use it: 50 | # 51 | # acts_as_authentic do |c| 52 | # c.crypto_provider = Authlogic::CryptoProviders::BCrypt 53 | # end 54 | # 55 | # You are good to go! 56 | class BCrypt 57 | class << self 58 | # This is the :cost option for the BCrpyt library. The higher the cost 59 | # the more secure it is and the longer is take the generate a hash. By 60 | # default this is 10. Set this to any value >= the engine's minimum 61 | # (currently 4), play around with it to get that perfect balance between 62 | # security and performance. 63 | def cost 64 | @cost ||= 10 65 | end 66 | 67 | def cost=(val) 68 | if val < ::BCrypt::Engine::MIN_COST 69 | raise ArgumentError, "Authlogic's bcrypt cost cannot be set below the engine's " \ 70 | "min cost (#{::BCrypt::Engine::MIN_COST})" 71 | end 72 | @cost = val 73 | end 74 | 75 | # Creates a BCrypt hash for the password passed. 76 | def encrypt(*tokens) 77 | ::BCrypt::Password.create(join_tokens(tokens), cost: cost) 78 | end 79 | 80 | # Does the hash match the tokens? Uses the same tokens that were used to 81 | # encrypt. 82 | def matches?(hash, *tokens) 83 | hash = new_from_hash(hash) 84 | return false if hash.blank? 85 | hash == join_tokens(tokens) 86 | end 87 | 88 | # This method is used as a flag to tell Authlogic to "resave" the 89 | # password upon a successful login, using the new cost 90 | def cost_matches?(hash) 91 | hash = new_from_hash(hash) 92 | if hash.blank? 93 | false 94 | else 95 | hash.cost == cost 96 | end 97 | end 98 | 99 | private 100 | 101 | def join_tokens(tokens) 102 | tokens.flatten.join 103 | end 104 | 105 | def new_from_hash(hash) 106 | ::BCrypt::Password.new(hash) 107 | rescue ::BCrypt::Errors::InvalidHash 108 | nil 109 | end 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/md5.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/md5" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | # A poor choice. There are known attacks against this algorithm. 8 | class MD5 9 | # V2 hashes the digest bytes in repeated stretches instead of hex characters. 10 | autoload :V2, File.join(__dir__, "md5", "v2") 11 | 12 | class << self 13 | attr_accessor :join_token 14 | 15 | # The number of times to loop through the encryption. 16 | def stretches 17 | @stretches ||= 1 18 | end 19 | attr_writer :stretches 20 | 21 | # Turns your raw password into a MD5 hash. 22 | def encrypt(*tokens) 23 | digest = tokens.flatten.join(join_token) 24 | stretches.times { digest = Digest::MD5.hexdigest(digest) } 25 | digest 26 | end 27 | 28 | # Does the crypted password match the tokens? Uses the same tokens that 29 | # were used to encrypt. 30 | def matches?(crypted, *tokens) 31 | encrypt(*tokens) == crypted 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/md5/v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/md5" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | class MD5 8 | # A poor choice. There are known attacks against this algorithm. 9 | class V2 10 | class << self 11 | attr_accessor :join_token 12 | 13 | # The number of times to loop through the encryption. 14 | def stretches 15 | @stretches ||= 1 16 | end 17 | attr_writer :stretches 18 | 19 | # Turns your raw password into a MD5 hash. 20 | def encrypt(*tokens) 21 | digest = tokens.flatten.join(join_token) 22 | stretches.times { digest = Digest::MD5.digest(digest) } 23 | digest.unpack1("H*") 24 | end 25 | 26 | # Does the crypted password match the tokens? Uses the same tokens that 27 | # were used to encrypt. 28 | def matches?(crypted, *tokens) 29 | encrypt(*tokens) == crypted 30 | end 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/scrypt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "scrypt" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | # SCrypt is the default provider for Authlogic. It is the only 8 | # choice in the adaptive hash family that accounts for hardware 9 | # based attacks by compensating with memory bound as well as cpu 10 | # bound computational constraints. It offers the same guarantees 11 | # as BCrypt in the way of one-way, unique and slow. 12 | # 13 | # Decided SCrypt is for you? Just install the scrypt gem: 14 | # 15 | # gem install scrypt 16 | # 17 | # Tell acts_as_authentic to use it: 18 | # 19 | # acts_as_authentic do |c| 20 | # c.crypto_provider = Authlogic::CryptoProviders::SCrypt 21 | # end 22 | class SCrypt 23 | class << self 24 | DEFAULTS = { 25 | key_len: 32, 26 | salt_size: 8, 27 | max_time: 0.2, 28 | max_mem: 1024 * 1024, 29 | max_memfrac: 0.5 30 | }.freeze 31 | 32 | attr_writer :key_len, :salt_size, :max_time, :max_mem, :max_memfrac 33 | # Key length - length in bytes of generated key, from 16 to 512. 34 | def key_len 35 | @key_len ||= DEFAULTS[:key_len] 36 | end 37 | 38 | # Salt size - size in bytes of random salt, from 8 to 32 39 | def salt_size 40 | @salt_size ||= DEFAULTS[:salt_size] 41 | end 42 | 43 | # Max time - maximum time spent in computation 44 | def max_time 45 | @max_time ||= DEFAULTS[:max_time] 46 | end 47 | 48 | # Max memory - maximum memory usage. The minimum is always 1MB 49 | def max_mem 50 | @max_mem ||= DEFAULTS[:max_mem] 51 | end 52 | 53 | # Max memory fraction - maximum memory out of all available. Always 54 | # greater than zero and <= 0.5. 55 | def max_memfrac 56 | @max_memfrac ||= DEFAULTS[:max_memfrac] 57 | end 58 | 59 | # Creates an SCrypt hash for the password passed. 60 | def encrypt(*tokens) 61 | ::SCrypt::Password.create( 62 | join_tokens(tokens), 63 | key_len: key_len, 64 | salt_size: salt_size, 65 | max_mem: max_mem, 66 | max_memfrac: max_memfrac, 67 | max_time: max_time 68 | ) 69 | end 70 | 71 | # Does the hash match the tokens? Uses the same tokens that were used to encrypt. 72 | def matches?(hash, *tokens) 73 | hash = new_from_hash(hash) 74 | return false if hash.blank? 75 | hash == join_tokens(tokens) 76 | end 77 | 78 | private 79 | 80 | def join_tokens(tokens) 81 | tokens.flatten.join 82 | end 83 | 84 | def new_from_hash(hash) 85 | ::SCrypt::Password.new(hash) 86 | rescue ::SCrypt::Errors::InvalidHash 87 | nil 88 | end 89 | end 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha1" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | # A poor choice. There are known attacks against this algorithm. 8 | class Sha1 9 | # V2 hashes the digest bytes in repeated stretches instead of hex characters. 10 | autoload :V2, File.join(__dir__, "sha1", "v2") 11 | 12 | class << self 13 | def join_token 14 | @join_token ||= "--" 15 | end 16 | attr_writer :join_token 17 | 18 | # The number of times to loop through the encryption. 19 | def stretches 20 | @stretches ||= 10 21 | end 22 | attr_writer :stretches 23 | 24 | # Turns your raw password into a Sha1 hash. 25 | def encrypt(*tokens) 26 | tokens = tokens.flatten 27 | digest = tokens.shift 28 | stretches.times do 29 | digest = Digest::SHA1.hexdigest([digest, *tokens].join(join_token)) 30 | end 31 | digest 32 | end 33 | 34 | # Does the crypted password match the tokens? Uses the same tokens that 35 | # were used to encrypt. 36 | def matches?(crypted, *tokens) 37 | encrypt(*tokens) == crypted 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha1/v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha1" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | class Sha1 8 | # A poor choice. There are known attacks against this algorithm. 9 | class V2 10 | class << self 11 | def join_token 12 | @join_token ||= "--" 13 | end 14 | attr_writer :join_token 15 | 16 | # The number of times to loop through the encryption. 17 | def stretches 18 | @stretches ||= 10 19 | end 20 | attr_writer :stretches 21 | 22 | # Turns your raw password into a Sha1 hash. 23 | def encrypt(*tokens) 24 | tokens = tokens.flatten 25 | digest = tokens.shift 26 | stretches.times do 27 | digest = Digest::SHA1.digest([digest, *tokens].join(join_token)) 28 | end 29 | digest.unpack1("H*") 30 | end 31 | 32 | # Does the crypted password match the tokens? Uses the same tokens that 33 | # were used to encrypt. 34 | def matches?(crypted, *tokens) 35 | encrypt(*tokens) == crypted 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha256.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha2" 4 | 5 | module Authlogic 6 | # The acts_as_authentic method has a crypto_provider option. This allows you 7 | # to use any type of encryption you like. Just create a class with a class 8 | # level encrypt and matches? method. See example below. 9 | # 10 | # === Example 11 | # 12 | # class MyAwesomeEncryptionMethod 13 | # def self.encrypt(*tokens) 14 | # # the tokens passed will be an array of objects, what type of object 15 | # # is irrelevant, just do what you need to do with them and return a 16 | # # single encrypted string. for example, you will most likely join all 17 | # # of the objects into a single string and then encrypt that string 18 | # end 19 | # 20 | # def self.matches?(crypted, *tokens) 21 | # # return true if the crypted string matches the tokens. Depending on 22 | # # your algorithm you might decrypt the string then compare it to the 23 | # # token, or you might encrypt the tokens and make sure it matches the 24 | # # crypted string, its up to you. 25 | # end 26 | # end 27 | module CryptoProviders 28 | # = Sha256 29 | # 30 | # Uses the Sha256 hash algorithm to encrypt passwords. 31 | class Sha256 32 | # V2 hashes the digest bytes in repeated stretches instead of hex characters. 33 | autoload :V2, File.join(__dir__, "sha256", "v2") 34 | 35 | class << self 36 | attr_accessor :join_token 37 | 38 | # The number of times to loop through the encryption. 39 | def stretches 40 | @stretches ||= 20 41 | end 42 | attr_writer :stretches 43 | 44 | # Turns your raw password into a Sha256 hash. 45 | def encrypt(*tokens) 46 | digest = tokens.flatten.join(join_token) 47 | stretches.times { digest = Digest::SHA256.hexdigest(digest) } 48 | digest 49 | end 50 | 51 | # Does the crypted password match the tokens? Uses the same tokens that 52 | # were used to encrypt. 53 | def matches?(crypted, *tokens) 54 | encrypt(*tokens) == crypted 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha256/v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha2" 4 | 5 | module Authlogic 6 | # The acts_as_authentic method has a crypto_provider option. This allows you 7 | # to use any type of encryption you like. Just create a class with a class 8 | # level encrypt and matches? method. See example below. 9 | # 10 | # === Example 11 | # 12 | # class MyAwesomeEncryptionMethod 13 | # def self.encrypt(*tokens) 14 | # # the tokens passed will be an array of objects, what type of object 15 | # # is irrelevant, just do what you need to do with them and return a 16 | # # single encrypted string. for example, you will most likely join all 17 | # # of the objects into a single string and then encrypt that string 18 | # end 19 | # 20 | # def self.matches?(crypted, *tokens) 21 | # # return true if the crypted string matches the tokens. Depending on 22 | # # your algorithm you might decrypt the string then compare it to the 23 | # # token, or you might encrypt the tokens and make sure it matches the 24 | # # crypted string, its up to you. 25 | # end 26 | # end 27 | module CryptoProviders 28 | class Sha256 29 | # = Sha256 30 | # 31 | # Uses the Sha256 hash algorithm to encrypt passwords. 32 | class V2 33 | class << self 34 | attr_accessor :join_token 35 | 36 | # The number of times to loop through the encryption. 37 | def stretches 38 | @stretches ||= 20 39 | end 40 | attr_writer :stretches 41 | 42 | # Turns your raw password into a Sha256 hash. 43 | def encrypt(*tokens) 44 | digest = tokens.flatten.join(join_token) 45 | stretches.times { digest = Digest::SHA256.digest(digest) } 46 | digest.unpack1("H*") 47 | end 48 | 49 | # Does the crypted password match the tokens? Uses the same tokens that 50 | # were used to encrypt. 51 | def matches?(crypted, *tokens) 52 | encrypt(*tokens) == crypted 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha512.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha2" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | # SHA-512 does not have any practical known attacks against it. However, 8 | # there are better choices. We recommend transitioning to a more secure, 9 | # adaptive hashing algorithm, like scrypt. 10 | class Sha512 11 | # V2 hashes the digest bytes in repeated stretches instead of hex characters. 12 | autoload :V2, File.join(__dir__, "sha512", "v2") 13 | 14 | class << self 15 | attr_accessor :join_token 16 | 17 | # The number of times to loop through the encryption. 18 | def stretches 19 | @stretches ||= 20 20 | end 21 | attr_writer :stretches 22 | 23 | # Turns your raw password into a Sha512 hash. 24 | def encrypt(*tokens) 25 | digest = tokens.flatten.join(join_token) 26 | stretches.times { digest = Digest::SHA512.hexdigest(digest) } 27 | digest 28 | end 29 | 30 | # Does the crypted password match the tokens? Uses the same tokens that 31 | # were used to encrypt. 32 | def matches?(crypted, *tokens) 33 | encrypt(*tokens) == crypted 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/authlogic/crypto_providers/sha512/v2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "digest/sha2" 4 | 5 | module Authlogic 6 | module CryptoProviders 7 | class Sha512 8 | # SHA-512 does not have any practical known attacks against it. However, 9 | # there are better choices. We recommend transitioning to a more secure, 10 | # adaptive hashing algorithm, like scrypt. 11 | class V2 12 | class << self 13 | attr_accessor :join_token 14 | 15 | # The number of times to loop through the encryption. 16 | def stretches 17 | @stretches ||= 20 18 | end 19 | attr_writer :stretches 20 | 21 | # Turns your raw password into a Sha512 hash. 22 | def encrypt(*tokens) 23 | digest = tokens.flatten.join(join_token) 24 | stretches.times do 25 | digest = Digest::SHA512.digest(digest) 26 | end 27 | digest.unpack1("H*") 28 | end 29 | 30 | # Does the crypted password match the tokens? Uses the same tokens that 31 | # were used to encrypt. 32 | def matches?(crypted, *tokens) 33 | encrypt(*tokens) == crypted 34 | end 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/authlogic/errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | # Parent class of all Authlogic errors. 5 | class Error < StandardError 6 | end 7 | 8 | # :nodoc: 9 | class InvalidCryptoProvider < Error 10 | end 11 | 12 | # :nodoc: 13 | class NilCryptoProvider < InvalidCryptoProvider 14 | def message 15 | <<~EOS 16 | In version 5, Authlogic used SCrypt by default. As of version 6, there 17 | is no default. We still recommend SCrypt. If you previously relied on 18 | this default, then, in your User model (or equivalent), please set the 19 | following: 20 | 21 | acts_as_authentic do |c| 22 | c.crypto_provider = ::Authlogic::CryptoProviders::SCrypt 23 | end 24 | 25 | Furthermore, the authlogic gem no longer depends on the scrypt gem. In 26 | your Gemfile, please add scrypt. 27 | 28 | gem "scrypt", "~> 3.0" 29 | 30 | We have made this change in Authlogic 6 so that users of other crypto 31 | providers no longer need to install the scrypt gem. 32 | EOS 33 | end 34 | end 35 | 36 | # :nodoc: 37 | class ModelSetupError < Error 38 | def message 39 | <<-EOS 40 | You must establish a database connection and run the migrations before 41 | using acts_as_authentic. If you need to load the User model before the 42 | database is set up correctly, please set the following: 43 | 44 | acts_as_authentic do |c| 45 | c.raise_on_model_setup_error = false 46 | end 47 | EOS 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/authlogic/i18n.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "i18n/translator" 4 | 5 | module Authlogic 6 | # This class allows any message in Authlogic to use internationalization. In 7 | # earlier versions of Authlogic each message was translated via configuration. 8 | # This cluttered up the configuration and cluttered up Authlogic. So all 9 | # translation has been extracted out into this class. Now all messages pass 10 | # through this class, making it much easier to implement in I18n library / 11 | # plugin you want. Use this as a layer that sits between Authlogic and 12 | # whatever I18n library you want to use. 13 | # 14 | # By default this uses the rails I18n library, if it exists. If it doesn't 15 | # exist it just returns the default English message. The Authlogic I18n class 16 | # works EXACTLY like the rails I18n class. This is because the arguments are 17 | # delegated to this class. 18 | # 19 | # Here is how all messages are translated internally with Authlogic: 20 | # 21 | # Authlogic::I18n.t('error_messages.password_invalid', :default => "is invalid") 22 | # 23 | # If you use a different I18n library just replace the build-in 24 | # I18n::Translator class with your own. For example: 25 | # 26 | # class MyAuthlogicI18nTranslator 27 | # def translate(key, options = {}) 28 | # # you will have key which will be something like: 29 | # # "error_messages.password_invalid" 30 | # # you will also have options[:default], which will be the default 31 | # # English version of the message 32 | # # do whatever you want here with the arguments passed to you. 33 | # end 34 | # end 35 | # 36 | # Authlogic::I18n.translator = MyAuthlogicI18nTranslator.new 37 | # 38 | # That it's! Here is a complete list of the keys that are passed. Just define 39 | # these however you wish: 40 | # 41 | # authlogic: 42 | # error_messages: 43 | # login_blank: can not be blank 44 | # login_not_found: is not valid 45 | # login_invalid: should use only letters, numbers, spaces, and .-_@+ please. 46 | # consecutive_failed_logins_limit_exceeded: > 47 | # Consecutive failed logins limit exceeded, account is disabled. 48 | # email_invalid: should look like an email address. 49 | # email_invalid_international: should look like an international email address. 50 | # password_blank: can not be blank 51 | # password_invalid: is not valid 52 | # not_active: Your account is not active 53 | # not_confirmed: Your account is not confirmed 54 | # not_approved: Your account is not approved 55 | # no_authentication_details: You did not provide any details for authentication. 56 | # general_credentials_error: Login/Password combination is not valid 57 | # session_invalid: Your session is invalid and has the following errors: 58 | # models: 59 | # user_session: UserSession (or whatever name you are using) 60 | # attributes: 61 | # user_session: (or whatever name you are using) 62 | # login: login 63 | # email: email 64 | # password: password 65 | # remember_me: remember me 66 | module I18n 67 | @@scope = :authlogic 68 | @@translator = nil 69 | 70 | class << self 71 | # Returns the current scope. Defaults to :authlogic 72 | def scope 73 | @@scope 74 | end 75 | 76 | # Sets the current scope. Used to set a custom scope. 77 | def scope=(scope) 78 | @@scope = scope 79 | end 80 | 81 | # Returns the current translator. Defaults to +Translator+. 82 | def translator 83 | @@translator ||= Translator.new 84 | end 85 | 86 | # Sets the current translator. Used to set a custom translator. 87 | def translator=(translator) 88 | @@translator = translator 89 | end 90 | 91 | # All message translation is passed to this method. The first argument is 92 | # the key for the message. The second is options, see the rails I18n 93 | # library for a list of options used. 94 | def translate(key, options = {}) 95 | translator.translate key, { scope: I18n.scope }.merge(options) 96 | end 97 | alias t translate 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/authlogic/i18n/translator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module I18n 5 | # The default translator used by authlogic/i18n.rb 6 | class Translator 7 | # If the I18n gem is present, calls +I18n.translate+ passing all 8 | # arguments, else returns +options[:default]+. 9 | def translate(key, options = {}) 10 | if defined?(::I18n) 11 | ::I18n.translate key, **options 12 | else 13 | options[:default] 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/authlogic/random.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "securerandom" 4 | 5 | module Authlogic 6 | # Generates random strings using ruby's SecureRandom library. 7 | module Random 8 | def self.hex_token 9 | SecureRandom.hex(64) 10 | end 11 | 12 | # Returns a string in base64url format as defined by RFC-3548 and RFC-4648. 13 | # We call this a "friendly" token because it is short and safe for URLs. 14 | def self.friendly_token 15 | SecureRandom.urlsafe_base64(15) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/authlogic/session/magic_column/assigns_last_request_at.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module Session 5 | module MagicColumn 6 | # Assigns the current time to the `last_request_at` attribute. 7 | # 8 | # 1. The `last_request_at` column must exist 9 | # 2. Assignment can be disabled on a per-controller basis 10 | # 3. Assignment will not happen more often than `last_request_at_threshold` 11 | # seconds. 12 | # 13 | # - current_time - a `Time` 14 | # - record - eg. a `User` 15 | # - controller - an `Authlogic::ControllerAdapters::AbstractAdapter` 16 | # - last_request_at_threshold - integer - seconds 17 | # 18 | # @api private 19 | class AssignsLastRequestAt 20 | def initialize(current_time, record, controller, last_request_at_threshold) 21 | @current_time = current_time 22 | @record = record 23 | @controller = controller 24 | @last_request_at_threshold = last_request_at_threshold 25 | end 26 | 27 | def assign 28 | return unless assign? 29 | @record.last_request_at = @current_time 30 | end 31 | 32 | private 33 | 34 | # @api private 35 | def assign? 36 | @record && 37 | @record.class.column_names.include?("last_request_at") && 38 | @controller.last_request_update_allowed? && ( 39 | @record.last_request_at.blank? || 40 | @last_request_at_threshold.to_i.seconds.ago >= @record.last_request_at 41 | ) 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/mock_api_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | # Basically acts like an API controller but doesn't do anything. 6 | # Authlogic can interact with this, do it's thing and then you can look at 7 | # the controller object to see if anything changed. 8 | class MockAPIController < ControllerAdapters::AbstractAdapter 9 | attr_writer :request_content_type 10 | 11 | def initialize 12 | end 13 | 14 | # Expected API controller has no cookies method. 15 | undef :cookies 16 | 17 | def cookie_domain 18 | nil 19 | end 20 | 21 | def logger 22 | @logger ||= MockLogger.new 23 | end 24 | 25 | def params 26 | @params ||= {} 27 | end 28 | 29 | def request 30 | @request ||= MockRequest.new(self) 31 | end 32 | 33 | def request_content_type 34 | @request_content_type ||= "text/html" 35 | end 36 | 37 | def session 38 | @session ||= {} 39 | end 40 | 41 | # If method is defined, it causes below behavior... 42 | # controller = Authlogic::ControllerAdapters::RailsAdapter.new( 43 | # Authlogic::TestCase::MockAPIController.new 44 | # ) 45 | # controller.responds_to_single_access_allowed? #=> true 46 | # controller.single_access_allowed? 47 | # #=> NoMethodError: undefined method `single_access_allowed?' for nil:NilClass 48 | # 49 | undef :single_access_allowed? 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/mock_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | # Basically acts like a controller but doesn't do anything. Authlogic can interact 6 | # with this, do it's thing and then you can look at the controller object to see if 7 | # anything changed. 8 | class MockController < ControllerAdapters::AbstractAdapter 9 | attr_accessor :http_user, :http_password, :realm 10 | attr_writer :request_content_type 11 | 12 | def initialize 13 | end 14 | 15 | def authenticate_with_http_basic 16 | yield http_user, http_password 17 | end 18 | 19 | def authenticate_or_request_with_http_basic(realm = "DefaultRealm") 20 | self.realm = realm 21 | @http_auth_requested = true 22 | yield http_user, http_password 23 | end 24 | 25 | def cookies 26 | @cookies ||= MockCookieJar.new 27 | end 28 | 29 | def cookie_domain 30 | nil 31 | end 32 | 33 | def logger 34 | @logger ||= MockLogger.new 35 | end 36 | 37 | def params 38 | @params ||= {} 39 | end 40 | 41 | def request 42 | @request ||= MockRequest.new(self) 43 | end 44 | 45 | def request_content_type 46 | @request_content_type ||= "text/html" 47 | end 48 | 49 | def session 50 | @session ||= {} 51 | end 52 | 53 | def http_auth_requested? 54 | @http_auth_requested ||= false 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/mock_cookie_jar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | # A mock of `ActionDispatch::Cookies::CookieJar`. 6 | # See action_dispatch/middleware/cookies.rb 7 | class MockCookieJar < Hash # :nodoc: 8 | attr_accessor :set_cookies 9 | 10 | def [](key) 11 | hash = super 12 | hash && hash[:value] 13 | end 14 | 15 | # @param options - "the cookie's value [usually a string] or a hash of 16 | # options as documented above [in action_dispatch/middleware/cookies.rb]" 17 | def []=(key, options) 18 | opt = cookie_options_to_hash(options) 19 | (@set_cookies ||= {})[key.to_s] = opt 20 | super(key, opt) 21 | end 22 | 23 | def delete(key, _options = {}) 24 | super(key) 25 | end 26 | 27 | def signed 28 | @signed ||= MockSignedCookieJar.new(self) 29 | end 30 | 31 | def encrypted 32 | @encrypted ||= MockEncryptedCookieJar.new(self) 33 | end 34 | 35 | private 36 | 37 | # @api private 38 | def cookie_options_to_hash(options) 39 | if options.is_a?(Hash) 40 | options 41 | else 42 | { value: options } 43 | end 44 | end 45 | end 46 | 47 | # A mock of `ActionDispatch::Cookies::SignedKeyRotatingCookieJar` 48 | # 49 | # > .. a jar that'll automatically generate a signed representation of 50 | # > cookie value and verify it when reading from the cookie again. 51 | # > actionpack/lib/action_dispatch/middleware/cookies.rb 52 | class MockSignedCookieJar < MockCookieJar 53 | attr_reader :parent_jar # helper for testing 54 | 55 | def initialize(parent_jar) 56 | @parent_jar = parent_jar 57 | parent_jar.each { |k, v| self[k] = v } 58 | end 59 | 60 | def [](val) 61 | signed_message = @parent_jar[val] 62 | if signed_message 63 | payload, signature = signed_message.split("--") 64 | raise "Invalid signature" unless Digest::SHA1.hexdigest(payload) == signature 65 | payload 66 | end 67 | end 68 | 69 | def []=(key, options) 70 | opt = cookie_options_to_hash(options) 71 | opt[:value] = "#{opt[:value]}--#{Digest::SHA1.hexdigest opt[:value]}" 72 | @parent_jar[key] = opt 73 | end 74 | end 75 | 76 | # Which ActionDispatch class is this a mock of? 77 | # TODO: Document as with other mocks above. 78 | class MockEncryptedCookieJar < MockCookieJar 79 | attr_reader :parent_jar # helper for testing 80 | 81 | def initialize(parent_jar) 82 | @parent_jar = parent_jar 83 | parent_jar.each { |k, v| self[k] = v } 84 | end 85 | 86 | def [](val) 87 | encrypted_message = @parent_jar[val] 88 | if encrypted_message 89 | self.class.decrypt(encrypted_message) 90 | end 91 | end 92 | 93 | def []=(key, options) 94 | opt = cookie_options_to_hash(options) 95 | opt[:value] = self.class.encrypt(opt[:value]) 96 | @parent_jar[key] = opt 97 | end 98 | 99 | # simple caesar cipher for testing 100 | def self.encrypt(str) 101 | str.unpack("U*").map(&:succ).pack("U*") 102 | end 103 | 104 | def self.decrypt(str) 105 | str.unpack("U*").map(&:pred).pack("U*") 106 | end 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/mock_logger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | # Simple class to replace real loggers, so that we can raise any errors being logged. 6 | class MockLogger 7 | def error(message) 8 | raise message 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/mock_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | class MockRequest # :nodoc: 6 | attr_accessor :controller 7 | 8 | def initialize(controller) 9 | self.controller = controller 10 | end 11 | 12 | def env 13 | @env ||= { 14 | ControllerAdapters::AbstractAdapter::ENV_SESSION_OPTIONS => {} 15 | } 16 | end 17 | 18 | def format 19 | controller.request_content_type if controller.respond_to? :request_content_type 20 | end 21 | 22 | def ip 23 | controller&.respond_to?(:env) && 24 | controller.env.is_a?(Hash) && 25 | controller.env["REMOTE_ADDR"] || 26 | "1.1.1.1" 27 | end 28 | 29 | private 30 | 31 | def method_missing(*args, &block) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/authlogic/test_case/rails_request_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authlogic 4 | module TestCase 5 | # Adapts authlogic to work with the @request object when testing. This way Authlogic 6 | # can set cookies and what not before a request is made, ultimately letting you log in 7 | # users in functional tests. 8 | class RailsRequestAdapter < ControllerAdapters::AbstractAdapter 9 | def authenticate_with_http_basic(&block) 10 | end 11 | 12 | def cookies 13 | new_cookies = MockCookieJar.new 14 | super.each do |key, value| 15 | new_cookies[key] = cookie_value(value) 16 | end 17 | new_cookies 18 | end 19 | 20 | def cookie_domain 21 | nil 22 | end 23 | 24 | def request 25 | @request ||= MockRequest.new(controller) 26 | end 27 | 28 | def request_content_type 29 | request.format.to_s 30 | end 31 | 32 | private 33 | 34 | def cookie_value(value) 35 | value.is_a?(Hash) ? value[:value] : value 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/authlogic/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # :nodoc: 4 | module Authlogic 5 | # Returns a `::Gem::Version`, the version number of the authlogic gem. 6 | # 7 | # It is preferable for a library to provide a `gem_version` method, rather 8 | # than a `VERSION` string, because `::Gem::Version` is easier to use in a 9 | # comparison. 10 | # 11 | # We cannot return a frozen `Version`, because rubygems will try to modify it. 12 | # https://github.com/binarylogic/authlogic/pull/590 13 | # 14 | # Added in 4.0.0 15 | # 16 | # @api public 17 | def self.gem_version 18 | ::Gem::Version.new("6.5.0") 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/base_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class BaseTest < ActiveSupport::TestCase 7 | def test_acts_as_authentic 8 | assert_nothing_raised do 9 | User.acts_as_authentic do 10 | end 11 | end 12 | end 13 | 14 | def test_acts_as_authentic_with_old_config 15 | assert_raise(ArgumentError) do 16 | User.acts_as_authentic({}) 17 | end 18 | end 19 | 20 | def test_acts_as_authentic_with_no_table_raise_on_model_setup_error_default 21 | klass = Class.new(ActiveRecord::Base) 22 | assert_nothing_raised do 23 | klass.acts_as_authentic 24 | end 25 | end 26 | 27 | def test_acts_as_authentic_with_no_table_raise_on_model_setup_error_enabled 28 | klass = Class.new(ActiveRecord::Base) 29 | e = assert_raises Authlogic::ModelSetupError do 30 | klass.acts_as_authentic do |c| 31 | c.raise_on_model_setup_error = true 32 | end 33 | end 34 | refute e.message.empty? 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/email_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class EmailTest < ActiveSupport::TestCase 7 | def test_email_field_config 8 | assert_equal :email, User.email_field 9 | assert_equal :email, Employee.email_field 10 | 11 | User.email_field = :nope 12 | assert_equal :nope, User.email_field 13 | User.email_field :email 14 | assert_equal :email, User.email_field 15 | end 16 | 17 | def test_deferred_error_message_translation 18 | # ensure we successfully loaded the test locale 19 | assert I18n.available_locales.include?(:lol), "Test locale failed to load" 20 | 21 | I18n.with_locale("lol") do 22 | cat = User.new 23 | cat.email = "meow" 24 | cat.validate 25 | assert_includes( 26 | cat.errors[:email], 27 | I18n.t("authlogic.error_messages.email_invalid") 28 | ) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/logged_in_status_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class LoggedInStatusTest < ActiveSupport::TestCase 7 | ERROR_MSG = "Multiple calls to %s should result in different relations" 8 | 9 | def test_logged_in_timeout_config 10 | assert_equal 10.minutes.to_i, User.logged_in_timeout 11 | assert_equal 10.minutes.to_i, Employee.logged_in_timeout 12 | 13 | User.logged_in_timeout = 1.hour 14 | assert_equal 1.hour.to_i, User.logged_in_timeout 15 | User.logged_in_timeout 10.minutes 16 | assert_equal 10.minutes.to_i, User.logged_in_timeout 17 | end 18 | 19 | def test_named_scope_logged_in 20 | # Testing that the scope returned differs, because the time it was called should be 21 | # slightly different. This is an attempt to make sure the scope is lambda wrapped 22 | # so that it is re-evaluated every time its called. My biggest concern is that the 23 | # test happens so fast that the test fails... I just don't know a better way to test it! 24 | 25 | # for rails 5 I've changed the where_values to to_sql to compare 26 | 27 | query1 = User.logged_in.to_sql 28 | sleep 0.1 29 | query2 = User.logged_in.to_sql 30 | assert query1 != query2, ERROR_MSG % "#logged_in" 31 | 32 | assert_equal 0, User.logged_in.count 33 | user = User.first 34 | user.last_request_at = Time.now 35 | user.current_login_at = Time.now 36 | user.save! 37 | assert_equal 1, User.logged_in.count 38 | end 39 | 40 | def test_named_scope_logged_out 41 | # Testing that the scope returned differs, because the time it was called should be 42 | # slightly different. This is an attempt to make sure the scope is lambda wrapped 43 | # so that it is re-evaluated every time its called. My biggest concern is that the 44 | # test happens so fast that the test fails... I just don't know a better way to test it! 45 | 46 | # for rails 5 I've changed the where_values to to_sql to compare 47 | 48 | assert User.logged_in.to_sql != User.logged_out.to_sql, ERROR_MSG % "#logged_out" 49 | 50 | assert_equal 3, User.logged_out.count 51 | User.first.update_attribute(:last_request_at, Time.now) 52 | assert_equal 2, User.logged_out.count 53 | end 54 | 55 | def test_logged_in_logged_out 56 | u = User.first 57 | refute u.logged_in? 58 | assert u.logged_out? 59 | u.last_request_at = Time.now 60 | assert u.logged_in? 61 | refute u.logged_out? 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/login_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | # Miscellaneous tests for configuration options related to the `login_field`. 7 | class MiscellaneousLoginTest < ActiveSupport::TestCase 8 | def test_login_field_config 9 | assert_equal :login, User.login_field 10 | assert_nil Employee.login_field 11 | 12 | User.login_field = :nope 13 | assert_equal :nope, User.login_field 14 | User.login_field :login 15 | assert_equal :login, User.login_field 16 | end 17 | 18 | def test_find_by_smart_case_login_field 19 | # `User` is configured to be case-sensitive. (It has a case-sensitive 20 | # uniqueness validation) 21 | ben = users(:ben) 22 | assert_equal ben, User.find_by_smart_case_login_field("bjohnson") 23 | assert_nil User.find_by_smart_case_login_field("BJOHNSON") 24 | assert_nil User.find_by_smart_case_login_field("Bjohnson") 25 | 26 | # Unlike `User`, `Employee` does not have a uniqueness validation. In 27 | # the absence of such, authlogic performs a case-insensitive query. 28 | drew = employees(:drew) 29 | assert_equal drew, Employee.find_by_smart_case_login_field("dgainor@binarylogic.com") 30 | assert_equal drew, Employee.find_by_smart_case_login_field("Dgainor@binarylogic.com") 31 | assert_equal drew, Employee.find_by_smart_case_login_field("DGAINOR@BINARYLOGIC.COM") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/magic_columns_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class MagicColumnsTest < ActiveSupport::TestCase 7 | def test_validates_numericality_of_login_count 8 | u = User.new 9 | u.login_count = -1 10 | refute u.valid? 11 | refute u.errors[:login_count].empty? 12 | 13 | u.login_count = 0 14 | refute u.valid? 15 | assert u.errors[:login_count].empty? 16 | end 17 | 18 | def test_validates_numericality_of_failed_login_count 19 | u = User.new 20 | u.failed_login_count = -1 21 | refute u.valid? 22 | refute u.errors[:failed_login_count].empty? 23 | 24 | u.failed_login_count = 0 25 | refute u.valid? 26 | assert u.errors[:failed_login_count].empty? 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/perishable_token_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class PerishableTokenTest < ActiveSupport::TestCase 7 | def test_perishable_token_valid_for_config 8 | assert_equal 10.minutes.to_i, User.perishable_token_valid_for 9 | assert_equal 10.minutes.to_i, Employee.perishable_token_valid_for 10 | 11 | User.perishable_token_valid_for = 1.hour 12 | assert_equal 1.hour.to_i, User.perishable_token_valid_for 13 | User.perishable_token_valid_for 10.minutes 14 | assert_equal 10.minutes.to_i, User.perishable_token_valid_for 15 | end 16 | 17 | def test_disable_perishable_token_maintenance_config 18 | refute User.disable_perishable_token_maintenance 19 | refute Employee.disable_perishable_token_maintenance 20 | 21 | User.disable_perishable_token_maintenance = true 22 | assert User.disable_perishable_token_maintenance 23 | User.disable_perishable_token_maintenance false 24 | refute User.disable_perishable_token_maintenance 25 | end 26 | 27 | def test_validates_uniqueness_of_perishable_token 28 | u = User.new 29 | u.perishable_token = users(:ben).perishable_token 30 | refute u.valid? 31 | refute u.errors[:perishable_token].empty? 32 | end 33 | 34 | def test_before_save_reset_perishable_token 35 | ben = users(:ben) 36 | old_perishable_token = ben.perishable_token 37 | assert ben.save 38 | assert_not_equal old_perishable_token, ben.perishable_token 39 | end 40 | 41 | def test_reset_perishable_token 42 | ben = users(:ben) 43 | old_perishable_token = ben.perishable_token 44 | 45 | assert ben.reset_perishable_token 46 | assert_not_equal old_perishable_token, ben.perishable_token 47 | 48 | ben.reload 49 | assert_equal old_perishable_token, ben.perishable_token 50 | 51 | assert ben.reset_perishable_token! 52 | assert_not_equal old_perishable_token, ben.perishable_token 53 | 54 | ben.reload 55 | assert_not_equal old_perishable_token, ben.perishable_token 56 | end 57 | 58 | def test_find_using_perishable_token 59 | ben = users(:ben) 60 | assert_equal ben, User.find_using_perishable_token(ben.perishable_token) 61 | end 62 | 63 | def test_find_using_perishable_token_when_perished 64 | ben = users(:ben) 65 | ActiveRecord::Base.connection.execute( 66 | "UPDATE users set updated_at = '#{1.week.ago.to_formatted_s(:db)}' where id = #{ben.id}" 67 | ) 68 | assert_nil User.find_using_perishable_token(ben.perishable_token) 69 | end 70 | 71 | def test_find_using_perishable_token_when_perished_2 72 | User.perishable_token_valid_for = 1.minute 73 | ben = users(:ben) 74 | ActiveRecord::Base.connection.execute( 75 | "UPDATE users set updated_at = '#{2.minutes.ago.to_formatted_s(:db)}' where id = #{ben.id}" 76 | ) 77 | assert_nil User.find_using_perishable_token(ben.perishable_token) 78 | User.perishable_token_valid_for = 10.minutes 79 | end 80 | 81 | def test_find_using_perishable_token_when_passing_threshold 82 | User.perishable_token_valid_for = 1.minute 83 | ben = users(:ben) 84 | ActiveRecord::Base.connection.execute( 85 | "UPDATE users set updated_at = '#{10.minutes.ago.to_formatted_s(:db)}' where id = #{ben.id}" 86 | ) 87 | assert_nil User.find_using_perishable_token(ben.perishable_token, 5.minutes) 88 | assert_equal ben, User.find_using_perishable_token(ben.perishable_token, 20.minutes) 89 | User.perishable_token_valid_for = 10.minutes 90 | end 91 | 92 | def test_find_perishable_token_with_bang 93 | assert_raises ActiveRecord::RecordNotFound do 94 | User.find_using_perishable_token!("some_bad_value") 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/persistence_token_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class PersistenceTokenTest < ActiveSupport::TestCase 7 | def test_after_password_set_reset_persistence_token 8 | ben = users(:ben) 9 | old_persistence_token = ben.persistence_token 10 | ben.password = "newpass" 11 | assert_not_equal old_persistence_token, ben.persistence_token 12 | end 13 | 14 | def test_after_password_verification_reset_persistence_token 15 | aaron = users(:aaron) 16 | old_persistence_token = aaron.persistence_token 17 | 18 | assert aaron.valid_password?(password_for(aaron)) 19 | assert_equal old_persistence_token, aaron.reload.persistence_token 20 | 21 | # only update it if it is nil 22 | assert aaron.update_attribute(:persistence_token, nil) 23 | assert aaron.valid_password?(password_for(aaron)) 24 | assert_not_equal old_persistence_token, aaron.persistence_token 25 | end 26 | 27 | def test_before_validate_reset_persistence_token 28 | u = User.new 29 | refute u.valid? 30 | assert_not_nil u.persistence_token 31 | end 32 | 33 | def test_forget_all 34 | UserSession.allow_http_basic_auth = true 35 | 36 | http_basic_auth_for(users(:ben)) { UserSession.find } 37 | http_basic_auth_for(users(:zack)) { UserSession.find(:ziggity_zack) } 38 | assert UserSession.find 39 | assert UserSession.find(:ziggity_zack) 40 | User.forget_all 41 | refute UserSession.find 42 | refute UserSession.find(:ziggity_zack) 43 | end 44 | 45 | def test_forget 46 | UserSession.allow_http_basic_auth = true 47 | 48 | ben = users(:ben) 49 | zack = users(:zack) 50 | http_basic_auth_for(ben) { UserSession.find } 51 | http_basic_auth_for(zack) { UserSession.find(:ziggity_zack) } 52 | 53 | assert ben.reload.logged_in? 54 | assert zack.reload.logged_in? 55 | 56 | ben.forget! 57 | 58 | refute UserSession.find 59 | assert UserSession.find(:ziggity_zack) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/session_maintenance_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class SessionMaintenanceTest < ActiveSupport::TestCase 7 | def setup 8 | User.log_in_after_create = true 9 | User.log_in_after_password_change = true 10 | end 11 | 12 | def test_log_in_after_create_config 13 | assert User.log_in_after_create 14 | User.log_in_after_create = false 15 | refute User.log_in_after_create 16 | User.log_in_after_create = true 17 | assert User.log_in_after_create 18 | end 19 | 20 | def test_log_in_after_password_change_config 21 | assert User.log_in_after_password_change 22 | User.log_in_after_password_change = false 23 | refute User.log_in_after_password_change 24 | User.log_in_after_password_change = true 25 | assert User.log_in_after_password_change 26 | end 27 | 28 | def test_login_after_create 29 | User.log_in_after_create = true 30 | user = User.create( 31 | login: "awesome", 32 | password: "saweeeet", 33 | password_confirmation: "saweeeet", 34 | email: "awesome@awesome.com" 35 | ) 36 | assert user.persisted? 37 | assert UserSession.find 38 | logged_in_user = UserSession.find.user 39 | assert_equal logged_in_user, user 40 | end 41 | 42 | def test_no_login_after_create 43 | old_user = User.create( 44 | login: "awesome", 45 | password: "saweeeet", 46 | password_confirmation: "saweeeet", 47 | email: "awesome@awesome.com" 48 | ) 49 | User.log_in_after_create = false 50 | user2 = User.create( 51 | login: "awesome2", 52 | password: "saweeeet2", 53 | password_confirmation: "saweeeet2", 54 | email: "awesome2@awesome.com" 55 | ) 56 | assert user2.persisted? 57 | logged_in_user = UserSession.find.user 58 | assert_not_equal logged_in_user, user2 59 | assert_equal logged_in_user, old_user 60 | end 61 | 62 | def test_no_login_after_logged_out_create 63 | User.log_in_after_create = false 64 | user = User.create( 65 | login: "awesome2", 66 | password: "saweeeet2", 67 | password_confirmation: "saweeeet2", 68 | email: "awesome2@awesome.com" 69 | ) 70 | assert user.persisted? 71 | assert_nil UserSession.find 72 | end 73 | 74 | def test_updating_session_with_failed_magic_state 75 | ben = users(:ben) 76 | ben.confirmed = false 77 | ben.password = "newpasswd" 78 | ben.password_confirmation = "newpasswd" 79 | assert ben.save 80 | end 81 | 82 | def test_update_session_after_password_modify 83 | User.log_in_after_password_change = true 84 | ben = users(:ben) 85 | UserSession.create(ben) 86 | old_session_key = controller.session["user_credentials"] 87 | old_cookie_key = controller.cookies["user_credentials"] 88 | ben.password = "newpasswd" 89 | ben.password_confirmation = "newpasswd" 90 | assert ben.save 91 | assert controller.session["user_credentials"] 92 | assert controller.cookies["user_credentials"] 93 | assert_not_equal controller.session["user_credentials"], old_session_key 94 | assert_not_equal controller.cookies["user_credentials"], old_cookie_key 95 | end 96 | 97 | def test_no_update_session_after_password_modify 98 | User.log_in_after_password_change = false 99 | ben = users(:ben) 100 | UserSession.create(ben) 101 | old_session_key = controller.session["user_credentials"] 102 | old_cookie_key = controller.cookies["user_credentials"] 103 | ben.password = "newpasswd" 104 | ben.password_confirmation = "newpasswd" 105 | assert ben.save 106 | assert controller.session["user_credentials"] 107 | assert controller.cookies["user_credentials"] 108 | assert_equal controller.session["user_credentials"], old_session_key 109 | assert_equal controller.cookies["user_credentials"], old_cookie_key 110 | end 111 | 112 | def test_no_session_update_after_modify 113 | ben = users(:ben) 114 | UserSession.create(ben) 115 | old_session_key = controller.session["user_credentials"] 116 | old_cookie_key = controller.cookies["user_credentials"] 117 | ben.first_name = "Ben" 118 | assert ben.save 119 | assert_equal controller.session["user_credentials"], old_session_key 120 | assert_equal controller.cookies["user_credentials"], old_cookie_key 121 | end 122 | 123 | def test_creating_other_user 124 | ben = users(:ben) 125 | UserSession.create(ben) 126 | old_session_key = controller.session["user_credentials"] 127 | old_cookie_key = controller.cookies["user_credentials"] 128 | user = User.create( 129 | login: "awesome", 130 | password: "saweet", # Password is too short, user invalid 131 | password_confirmation: "saweet", 132 | email: "awesome@saweet.com" 133 | ) 134 | refute user.persisted? 135 | assert_equal controller.session["user_credentials"], old_session_key 136 | assert_equal controller.cookies["user_credentials"], old_cookie_key 137 | end 138 | 139 | def test_updating_other_user 140 | ben = users(:ben) 141 | UserSession.create(ben) 142 | old_session_key = controller.session["user_credentials"] 143 | old_cookie_key = controller.cookies["user_credentials"] 144 | zack = users(:zack) 145 | zack.password = "newpasswd" 146 | zack.password_confirmation = "newpasswd" 147 | assert zack.save 148 | assert_equal controller.session["user_credentials"], old_session_key 149 | assert_equal controller.cookies["user_credentials"], old_cookie_key 150 | end 151 | 152 | def test_resetting_password_when_logged_out 153 | ben = users(:ben) 154 | refute UserSession.find 155 | ben.password = "newpasswd" 156 | ben.password_confirmation = "newpasswd" 157 | assert ben.save 158 | assert UserSession.find 159 | assert_equal ben, UserSession.find.record 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /test/acts_as_authentic_test/single_access_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module ActsAsAuthenticTest 6 | class SingleAccessTest < ActiveSupport::TestCase 7 | def test_change_single_access_token_with_password_config 8 | refute User.change_single_access_token_with_password 9 | refute Employee.change_single_access_token_with_password 10 | 11 | User.change_single_access_token_with_password = true 12 | assert User.change_single_access_token_with_password 13 | User.change_single_access_token_with_password false 14 | refute User.change_single_access_token_with_password 15 | end 16 | 17 | def test_validates_uniqueness_of_single_access_token 18 | u = User.new 19 | u.single_access_token = users(:ben).single_access_token 20 | refute u.valid? 21 | refute u.errors[:single_access_token].empty? 22 | end 23 | 24 | def test_before_validation_reset_single_access_token 25 | u = User.new 26 | refute u.valid? 27 | assert_not_nil u.single_access_token 28 | end 29 | 30 | def test_after_password_set_reset_single_access_token 31 | User.change_single_access_token_with_password = true 32 | 33 | ben = users(:ben) 34 | old_single_access_token = ben.single_access_token 35 | ben.password = "new_pass" 36 | assert_not_equal old_single_access_token, ben.single_access_token 37 | 38 | User.change_single_access_token_with_password = false 39 | end 40 | 41 | def test_after_password_set_is_not_called 42 | ldaper = Ldaper.new 43 | assert ldaper.save 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/adapter_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "authlogic/controller_adapters/rails_adapter" 5 | 6 | module Authlogic 7 | module ControllerAdapters 8 | class AbstractAdapterTest < ActiveSupport::TestCase 9 | def test_controller 10 | controller = Class.new(MockController) do 11 | def controller.an_arbitrary_method 12 | "bar" 13 | end 14 | end.new 15 | adapter = Authlogic::ControllerAdapters::AbstractAdapter.new(controller) 16 | 17 | assert_equal controller, adapter.controller 18 | assert controller.params.equal?(adapter.params) 19 | assert adapter.respond_to?(:an_arbitrary_method) 20 | assert_equal "bar", adapter.an_arbitrary_method 21 | end 22 | end 23 | 24 | class RailsAdapterTest < ActiveSupport::TestCase 25 | def test_api_controller 26 | controller = MockAPIController.new 27 | adapter = Authlogic::ControllerAdapters::RailsAdapter.new(controller) 28 | 29 | assert_equal controller, adapter.controller 30 | assert_nil adapter.cookies 31 | end 32 | end 33 | 34 | class RailsRequestAdapterTest < ActiveSupport::TestCase 35 | def test_adapter_with_string_cookie 36 | controller = MockController.new 37 | controller.cookies["foo"] = "bar" 38 | adapter = Authlogic::TestCase::RailsRequestAdapter.new(controller) 39 | 40 | assert adapter.cookies 41 | end 42 | 43 | def test_adapter_with_hash_cookie 44 | controller = MockController.new 45 | controller.cookies["foo"] = { 46 | value: "bar", 47 | expires: nil 48 | } 49 | adapter = Authlogic::TestCase::RailsRequestAdapter.new(controller) 50 | 51 | assert adapter.cookies 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/config_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ConfigTest < ActiveSupport::TestCase 6 | def setup 7 | @klass = Class.new { 8 | extend Authlogic::Config 9 | 10 | def self.foobar(value = nil) 11 | rw_config(:foobar_field, value, "default_foobar") 12 | end 13 | } 14 | 15 | @subklass = Class.new(@klass) 16 | end 17 | 18 | def test_config 19 | assert_equal({}, @klass.acts_as_authentic_config) 20 | end 21 | 22 | def test_rw_config_read_with_default 23 | assert "default_foobar", @klass.foobar 24 | end 25 | 26 | def test_rw_config_write 27 | assert_equal "my_foobar", @klass.foobar("my_foobar") 28 | assert_equal "my_foobar", @klass.foobar 29 | 30 | assert_equal "my_new_foobar", @klass.foobar("my_new_foobar") 31 | assert_equal "my_new_foobar", @klass.foobar 32 | end 33 | 34 | def test_subclass_rw_config_write 35 | assert_equal "subklass_foobar", @subklass.foobar("subklass_foobar") 36 | assert_equal "default_foobar", @klass.foobar 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/crypto_provider_test/bcrypt_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class BCryptTest < ActiveSupport::TestCase 7 | def test_encrypt 8 | assert Authlogic::CryptoProviders::BCrypt.encrypt("mypass") 9 | end 10 | 11 | def test_matches 12 | hash = Authlogic::CryptoProviders::BCrypt.encrypt("mypass") 13 | assert Authlogic::CryptoProviders::BCrypt.matches?(hash, "mypass") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/crypto_provider_test/md5/v2_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | module MD5 7 | class V2Test < ActiveSupport::TestCase 8 | def setup 9 | @default_stretches = Authlogic::CryptoProviders::MD5::V2.stretches 10 | end 11 | 12 | def teardown 13 | Authlogic::CryptoProviders::MD5::V2.stretches = @default_stretches 14 | end 15 | 16 | def test_encrypt 17 | password = "test" 18 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 19 | expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" 20 | 21 | digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt) 22 | 23 | assert_equal digest, expected_digest 24 | end 25 | 26 | def test_encrypt_with_3_stretches 27 | Authlogic::CryptoProviders::MD5::V2.stretches = 3 28 | password = "test" 29 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 30 | expected_digest = "da62ac8b983606f684cea0b93a558283" 31 | 32 | digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt) 33 | 34 | assert_equal digest, expected_digest 35 | end 36 | 37 | def test_matches 38 | password = "test" 39 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 40 | expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" 41 | 42 | assert Authlogic::CryptoProviders::MD5::V2.matches?(expected_digest, password, salt) 43 | end 44 | 45 | def test_not_matches 46 | password = "test" 47 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 48 | bad_digest = "12345" 49 | 50 | assert !Authlogic::CryptoProviders::MD5::V2.matches?(bad_digest, password, salt) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/crypto_provider_test/md5_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class MD5Test < ActiveSupport::TestCase 7 | def setup 8 | @default_stretches = Authlogic::CryptoProviders::MD5.stretches 9 | end 10 | 11 | def teardown 12 | Authlogic::CryptoProviders::MD5.stretches = @default_stretches 13 | end 14 | 15 | def test_encrypt 16 | password = "test" 17 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 18 | expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" 19 | 20 | digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt) 21 | 22 | assert_equal digest, expected_digest 23 | end 24 | 25 | def test_encrypt_with_3_stretches 26 | Authlogic::CryptoProviders::MD5.stretches = 3 27 | password = "test" 28 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 29 | expected_digest = "9ac3a3a2e68f822f3482cbea3cbed9a3" 30 | 31 | digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt) 32 | 33 | assert_equal digest, expected_digest 34 | end 35 | 36 | def test_matches 37 | password = "test" 38 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 39 | expected_digest = "3d16884295a68fec30a2ae7ff0634b1e" 40 | 41 | assert Authlogic::CryptoProviders::MD5.matches?(expected_digest, password, salt) 42 | end 43 | 44 | def test_not_matches 45 | password = "test" 46 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 47 | bad_digest = "12345" 48 | 49 | assert !Authlogic::CryptoProviders::MD5.matches?(bad_digest, password, salt) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/crypto_provider_test/scrypt_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class SCryptTest < ActiveSupport::TestCase 7 | def test_encrypt 8 | assert Authlogic::CryptoProviders::SCrypt.encrypt("mypass") 9 | end 10 | 11 | def test_matches 12 | hash = Authlogic::CryptoProviders::SCrypt.encrypt("mypass") 13 | assert Authlogic::CryptoProviders::SCrypt.matches?(hash, "mypass") 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha1/v2_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | module VSHA1 7 | class V2Test < ActiveSupport::TestCase 8 | def setup 9 | @default_stretches = Authlogic::CryptoProviders::Sha1::V2.stretches 10 | end 11 | 12 | def teardown 13 | Authlogic::CryptoProviders::Sha1::V2.stretches = @default_stretches 14 | end 15 | 16 | def test_encrypt 17 | password = "test" 18 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 19 | expected_digest = "12d995b1f0af7d24d6f89d2e63dfbcb752384815" 20 | 21 | digest = Authlogic::CryptoProviders::Sha1::V2.encrypt(password, salt) 22 | 23 | assert_equal digest, expected_digest 24 | end 25 | 26 | def test_encrypt_with_3_stretches 27 | Authlogic::CryptoProviders::Sha1::V2.stretches = 3 28 | password = "test" 29 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 30 | expected_digest = "af1e00f841ccc742c1e5879af35ca02b1978a1ac" 31 | 32 | digest = Authlogic::CryptoProviders::Sha1::V2.encrypt(password, salt) 33 | 34 | assert_equal digest, expected_digest 35 | end 36 | 37 | def test_matches 38 | password = "test" 39 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 40 | expected_digest = "12d995b1f0af7d24d6f89d2e63dfbcb752384815" 41 | 42 | assert Authlogic::CryptoProviders::Sha1::V2.matches?(expected_digest, password, salt) 43 | end 44 | 45 | def test_not_matches 46 | password = "test" 47 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 48 | bad_digest = "12345" 49 | 50 | assert !Authlogic::CryptoProviders::Sha1::V2.matches?(bad_digest, password, salt) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha1_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class Sha1Test < ActiveSupport::TestCase 7 | def setup 8 | @default_stretches = Authlogic::CryptoProviders::Sha1.stretches 9 | end 10 | 11 | def teardown 12 | Authlogic::CryptoProviders::Sha1.stretches = @default_stretches 13 | end 14 | 15 | def test_encrypt 16 | password = "test" 17 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 18 | expected_digest = "5723d69f7ca1f8d63122c9cef4cf3c10d0482d3e" 19 | 20 | digest = Authlogic::CryptoProviders::Sha1.encrypt(password, salt) 21 | 22 | assert_equal digest, expected_digest 23 | end 24 | 25 | def test_encrypt_with_3_stretches 26 | Authlogic::CryptoProviders::Sha1.stretches = 3 27 | password = "test" 28 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 29 | expected_digest = "969f681d90a7d25679256e38cce3dc10db6d49c5" 30 | 31 | digest = Authlogic::CryptoProviders::Sha1.encrypt(password, salt) 32 | 33 | assert_equal digest, expected_digest 34 | end 35 | 36 | def test_matches 37 | password = "test" 38 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 39 | expected_digest = "5723d69f7ca1f8d63122c9cef4cf3c10d0482d3e" 40 | 41 | assert Authlogic::CryptoProviders::Sha1.matches?(expected_digest, password, salt) 42 | end 43 | 44 | def test_not_matches 45 | password = "test" 46 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 47 | bad_digest = "12345" 48 | 49 | assert !Authlogic::CryptoProviders::Sha1.matches?(bad_digest, password, salt) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha256/v2_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | module SHA256 7 | class V2Test < ActiveSupport::TestCase 8 | def setup 9 | @default_stretches = Authlogic::CryptoProviders::Sha256::V2.stretches 10 | end 11 | 12 | def teardown 13 | Authlogic::CryptoProviders::Sha256::V2.stretches = @default_stretches 14 | end 15 | 16 | def test_encrypt 17 | password = "test" 18 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 19 | expected_digest = "7f42a368b64a3c284c87b3ed3145b0c89f6bc49de931ca083e9c56a5c6b98e22" 20 | 21 | digest = Authlogic::CryptoProviders::Sha256::V2.encrypt(password, salt) 22 | 23 | assert_equal digest, expected_digest 24 | end 25 | 26 | def test_encrypt_with_3_stretches 27 | Authlogic::CryptoProviders::Sha256::V2.stretches = 3 28 | password = "test" 29 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 30 | expected_digest = "1560ebc3b08d86828a7e9267379f7dbb847b6cc255135fc13210a4155a58b981" 31 | 32 | digest = Authlogic::CryptoProviders::Sha256::V2.encrypt(password, salt) 33 | 34 | assert_equal digest, expected_digest 35 | end 36 | 37 | def test_matches 38 | password = "test" 39 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 40 | expected_digest = "7f42a368b64a3c284c87b3ed3145b0c89f6bc49de931ca083e9c56a5c6b98e22" 41 | 42 | assert Authlogic::CryptoProviders::Sha256::V2.matches?(expected_digest, password, salt) 43 | end 44 | 45 | def test_not_matches 46 | password = "test" 47 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 48 | bad_digest = "12345" 49 | 50 | assert !Authlogic::CryptoProviders::Sha256::V2.matches?(bad_digest, password, salt) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha256_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class Sha256Test < ActiveSupport::TestCase 7 | def setup 8 | @default_stretches = Authlogic::CryptoProviders::Sha256.stretches 9 | end 10 | 11 | def teardown 12 | Authlogic::CryptoProviders::Sha256.stretches = @default_stretches 13 | end 14 | 15 | def test_encrypt 16 | password = "test" 17 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 18 | expected_digest = "3c4f802953726704088a3cd6d89237e9a279a8e8f43fa6de8549ca54b80b766c" 19 | 20 | digest = Authlogic::CryptoProviders::Sha256.encrypt(password, salt) 21 | 22 | assert_equal digest, expected_digest 23 | end 24 | 25 | def test_encrypt_with_3_stretches 26 | Authlogic::CryptoProviders::Sha256.stretches = 3 27 | password = "test" 28 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 29 | expected_digest = "06a2e9cd5552f2cdbc01ec61d52ce80d0bfba8f1bb689a356ac0193d42adc831" 30 | 31 | digest = Authlogic::CryptoProviders::Sha256.encrypt(password, salt) 32 | 33 | assert_equal digest, expected_digest 34 | end 35 | 36 | def test_matches 37 | password = "test" 38 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 39 | expected_digest = "3c4f802953726704088a3cd6d89237e9a279a8e8f43fa6de8549ca54b80b766c" 40 | 41 | assert Authlogic::CryptoProviders::Sha256.matches?(expected_digest, password, salt) 42 | end 43 | 44 | def test_not_matches 45 | password = "test" 46 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 47 | bad_digest = "12345" 48 | 49 | assert !Authlogic::CryptoProviders::Sha256.matches?(bad_digest, password, salt) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha512/v2_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | module SHA512 7 | class V2Test < ActiveSupport::TestCase 8 | def setup 9 | @default_stretches = Authlogic::CryptoProviders::Sha512::V2.stretches 10 | end 11 | 12 | def teardown 13 | Authlogic::CryptoProviders::Sha512::V2.stretches = @default_stretches 14 | end 15 | 16 | def test_encrypt 17 | password = "test" 18 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 19 | expected_digest = "60e86eec0e7f858cc5cc6b42b31a847819b65e06317709ce27" \ 20 | "79245d0776f18094dff9afbc66ae1e509f2b5e49f4d2ff3f632c8ee7c4683749f5fd028de5b085" 21 | 22 | digest = Authlogic::CryptoProviders::Sha512::V2.encrypt(password, salt) 23 | 24 | assert_equal digest, expected_digest 25 | end 26 | 27 | def test_encrypt_with_3_stretches 28 | Authlogic::CryptoProviders::Sha512::V2.stretches = 3 29 | password = "test" 30 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 31 | expected_digest = "c4f546026f67a4fcce0e4df5905b845f75d9cfe1371eeaba99" \ 32 | "a2c045940a7d08aa81837344752a9d4fb93883402114edd03955ed5962cd89b6e335c2ec5ca4a5" 33 | digest = Authlogic::CryptoProviders::Sha512::V2.encrypt(password, salt) 34 | 35 | assert_equal digest, expected_digest 36 | end 37 | 38 | def test_matches 39 | password = "test" 40 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 41 | expected_digest = "60e86eec0e7f858cc5cc6b42b31a847819b65e06317709ce27" \ 42 | "79245d0776f18094dff9afbc66ae1e509f2b5e49f4d2ff3f632c8ee7c4683749f5fd028de5b085" 43 | assert Authlogic::CryptoProviders::Sha512::V2.matches?(expected_digest, password, salt) 44 | end 45 | 46 | def test_not_matches 47 | password = "test" 48 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 49 | bad_digest = "12345" 50 | 51 | assert !Authlogic::CryptoProviders::Sha512::V2.matches?(bad_digest, password, salt) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/crypto_provider_test/sha512_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module CryptoProviderTest 6 | class Sha512Test < ActiveSupport::TestCase 7 | def setup 8 | @default_stretches = Authlogic::CryptoProviders::Sha512.stretches 9 | end 10 | 11 | def teardown 12 | Authlogic::CryptoProviders::Sha512.stretches = @default_stretches 13 | end 14 | 15 | def test_encrypt 16 | password = "test" 17 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 18 | expected_digest = "9508ba2964d65501aa1d7798e8f250b35f50fadb870871f2bc1f" \ 19 | "390872e8456e785633d06e17ffa4984a04cfa1a0e1ec29f15c31187b991e591393c6c0bffb61" 20 | digest = Authlogic::CryptoProviders::Sha512.encrypt(password, salt) 21 | assert_equal digest, expected_digest 22 | end 23 | 24 | def test_encrypt_with_3_stretches 25 | Authlogic::CryptoProviders::Sha512.stretches = 3 26 | password = "test" 27 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 28 | expected_digest = "ed507752ef2e985a9e5661fedcbac8ad7536d4b80c87183c2027" \ 29 | "3f568afb6f2112886fd786de00458eb2a14c640d9060c4688825e715cc1c3ecde8997d4ae556" 30 | digest = Authlogic::CryptoProviders::Sha512.encrypt(password, salt) 31 | assert_equal digest, expected_digest 32 | end 33 | 34 | def test_matches 35 | password = "test" 36 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 37 | expected_digest = "9508ba2964d65501aa1d7798e8f250b35f50fadb870871f2bc1f" \ 38 | "390872e8456e785633d06e17ffa4984a04cfa1a0e1ec29f15c31187b991e591393c6c0bffb61" 39 | assert Authlogic::CryptoProviders::Sha512.matches?(expected_digest, password, salt) 40 | end 41 | 42 | def test_not_matches 43 | password = "test" 44 | salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" 45 | bad_digest = "12345" 46 | assert !Authlogic::CryptoProviders::Sha512.matches?(bad_digest, password, salt) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/fixtures/admins.yml: -------------------------------------------------------------------------------- 1 | lumbergh: 2 | login: lumbergh 3 | crypted_password: <%= Authlogic::CryptoProviders::SCrypt.encrypt("lumberghrocks") %> 4 | password_salt: <%= salt = Authlogic::Random.hex_token %> 5 | persistence_token: e3d853f5aa0dacac5c257d03c4e097a3a7f51b182a8fc4f62096d05e939b019855aff0290157ac854e4195f13284ff5223f1996d0fd073e7e360171de54db278 6 | perishable_token: <%= Authlogic::Random.friendly_token %> 7 | email: lumbergh@initech.com 8 | role: TPS Supervisor 9 | -------------------------------------------------------------------------------- /test/fixtures/companies.yml: -------------------------------------------------------------------------------- 1 | binary_logic: 2 | name: Binary Logic 3 | 4 | logic_over_data: 5 | name: Logic Over Data 6 | -------------------------------------------------------------------------------- /test/fixtures/employees.yml: -------------------------------------------------------------------------------- 1 | drew: 2 | company: binary_logic 3 | email: dgainor@binarylogic.com 4 | password_salt: <%= salt = Authlogic::Random.hex_token %> 5 | crypted_password: '<%= Employee.crypto_provider.encrypt("drewrocks" + salt) %>' 6 | persistence_token: 5273d85ed156e9dbd6a7c1438d319ef8c8d41dd24368db6c222de11346c7b11e53ee08d45ecf619b1c1dc91233d22b372482b751b066d0a6f6f9bac42eacaabf 7 | first_name: Drew 8 | last_name: Gainor 9 | 10 | jennifer: 11 | company: logic_over_data 12 | email: jjohnson@logicoverdata.com 13 | password_salt: <%= salt = Authlogic::Random.hex_token %> 14 | crypted_password: '<%= Employee.crypto_provider.encrypt("jenniferocks" + salt) %>' 15 | persistence_token: 2be52a8f741ad00056e6f94eb6844d5316527206da7a3a5e3d0e14d19499ef9fe4c47c89b87febb59a2b41a69edfb4733b6b79302040f3de83f297c6991c75a2 16 | first_name: Jennifer 17 | last_name: Johnson 18 | -------------------------------------------------------------------------------- /test/fixtures/projects.yml: -------------------------------------------------------------------------------- 1 | web_services: 2 | name: web services 3 | users: ben, zack -------------------------------------------------------------------------------- /test/fixtures/users.yml: -------------------------------------------------------------------------------- 1 | # NB :ben and :zack use the legacy crypto provider (Sha512) ... when they're 2 | # tested for valid_password?() it will transition their password 3 | # (re: test/libs/user.rb). This could have unintended side-effects (like auto- 4 | # resetting their persistence token when checking password) -- one solution 5 | # is to just switch in users(:aaron) for those tests. 6 | ben: 7 | company: binary_logic 8 | projects: web_services 9 | login: bjohnson 10 | password_salt: <%= salt = Authlogic::Random.hex_token %> 11 | crypted_password: <%= Authlogic::CryptoProviders::Sha512.encrypt("benrocks" + salt) %> 12 | persistence_token: 6cde0674657a8a313ce952df979de2830309aa4c11ca65805dd00bfdc65dbcc2f5e36718660a1d2e68c1a08c276d996763985d2f06fd3d076eb7bc4d97b1e317 13 | single_access_token: <%= Authlogic::Random.friendly_token %> 14 | perishable_token: <%= Authlogic::Random.friendly_token %> 15 | email: bjohnson@binarylogic.com 16 | first_name: Ben 17 | last_name: Johnson 18 | 19 | zack: 20 | company: logic_over_data 21 | projects: web_services 22 | login: zackham 23 | password_salt: <%= salt = Authlogic::Random.hex_token %> 24 | crypted_password: <%= Authlogic::CryptoProviders::Sha512.encrypt("zackrocks" + salt) %> 25 | persistence_token: fd3c2d5ce09ab98e7547d21f1b3dcf9158a9a19b5d3022c0402f32ae197019fce3fdbc6614d7ee57d719bae53bb089e30edc9e5d6153e5bc3afca0ac1d320342 26 | single_access_token: <%= Authlogic::Random.friendly_token %> 27 | email: zham@ziggityzack.com 28 | first_name: Zack 29 | last_name: Ham 30 | 31 | aaron: 32 | company: cigital 33 | projects: web_services 34 | login: abedra 35 | crypted_password: <%= Authlogic::CryptoProviders::SCrypt.encrypt("aaronrocks") %> 36 | persistence_token: e3d853f5aa0dacac5c257d03c4e097a3a7f51b182a8fc4f62096d05e939b019855aff0290157ac854e4195f13284ff5223f1996d0fd073e7e360171de54db278 37 | single_access_token: <%= Authlogic::Random.friendly_token %> 38 | perishable_token: <%= Authlogic::Random.friendly_token %> 39 | email: abedra@cigital.com 40 | first_name: Aaron 41 | last_name: Bedra 42 | -------------------------------------------------------------------------------- /test/i18n/lol.yml: -------------------------------------------------------------------------------- 1 | lol: 2 | authlogic: 3 | error_messages: 4 | email_invalid: LOL email should be valid. 5 | -------------------------------------------------------------------------------- /test/i18n_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class I18nTest < ActiveSupport::TestCase 6 | def test_uses_authlogic_as_scope_by_default 7 | assert_equal :authlogic, Authlogic::I18n.scope 8 | end 9 | 10 | def test_can_set_scope 11 | assert_nothing_raised { Authlogic::I18n.scope = %i[a b] } 12 | assert_equal %i[a b], Authlogic::I18n.scope 13 | Authlogic::I18n.scope = :authlogic 14 | end 15 | 16 | def test_uses_built_in_translator_by_default 17 | assert_equal Authlogic::I18n::Translator, Authlogic::I18n.translator.class 18 | end 19 | 20 | def test_can_set_custom_translator 21 | old_translator = Authlogic::I18n.translator 22 | 23 | assert_nothing_raised do 24 | Authlogic::I18n.translator = Class.new do 25 | def translate(key, _options = {}) 26 | "Translated: #{key}" 27 | end 28 | end.new 29 | end 30 | 31 | assert_equal "Translated: x", Authlogic::I18n.translate(:x) 32 | 33 | Authlogic::I18n.translator = old_translator 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/libs/admin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This model demonstrates an `after_save` callback. 4 | class Admin < ActiveRecord::Base 5 | acts_as_authentic do |c| 6 | c.crypto_provider = Authlogic::CryptoProviders::SCrypt 7 | end 8 | 9 | validates :password, confirmation: true 10 | 11 | after_save do 12 | # In rails 5.1 `role_changed?` was deprecated in favor of `saved_change_to_role?`. 13 | # 14 | # > DEPRECATION WARNING: The behavior of `attribute_changed?` inside of 15 | # > after callbacks will be changing in the next version of Rails. 16 | # > The new return value will reflect the behavior of calling the method 17 | # > after `save` returned (e.g. the opposite of what it returns now). To 18 | # > maintain the current behavior, use `saved_change_to_attribute?` instead. 19 | # 20 | # So, in rails >= 5.2, we must use `saved_change_to_role?`. 21 | if saved_change_to_role? 22 | reset_password! 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/libs/admin_session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AdminSession < Authlogic::Session::Base 4 | end 5 | -------------------------------------------------------------------------------- /test/libs/affiliate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Affiliate < ActiveRecord::Base 4 | acts_as_authentic do |c| 5 | c.crypted_password_field = :pw_hash 6 | end 7 | 8 | belongs_to :company 9 | end 10 | -------------------------------------------------------------------------------- /test/libs/company.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Company < ActiveRecord::Base 4 | has_many :employees, dependent: :destroy 5 | has_many :users, dependent: :destroy 6 | end 7 | -------------------------------------------------------------------------------- /test/libs/employee.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Employee < ActiveRecord::Base 4 | acts_as_authentic do |config| 5 | silence_warnings do 6 | config.crypto_provider = Authlogic::CryptoProviders::Sha512 7 | end 8 | end 9 | belongs_to :company 10 | end 11 | -------------------------------------------------------------------------------- /test/libs/employee_session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EmployeeSession < Authlogic::Session::Base 4 | end 5 | -------------------------------------------------------------------------------- /test/libs/ldaper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Ldaper < ActiveRecord::Base 4 | acts_as_authentic 5 | end 6 | -------------------------------------------------------------------------------- /test/libs/project.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Project < ActiveRecord::Base 4 | has_and_belongs_to_many :users 5 | end 6 | -------------------------------------------------------------------------------- /test/libs/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ActiveRecord::Base 4 | EMAIL = / 5 | \A 6 | [A-Z0-9_.&%+\-']+ # mailbox 7 | @ 8 | (?:[A-Z0-9\-]+\.)+ # subdomains 9 | (?:[A-Z]{2,25}) # TLD 10 | \z 11 | /ix.freeze 12 | LOGIN = /\A[a-zA-Z0-9_][a-zA-Z0-9\.+\-_@ ]+\z/.freeze 13 | 14 | acts_as_authentic do |c| 15 | c.crypto_provider = Authlogic::CryptoProviders::SCrypt 16 | c.transition_from_crypto_providers Authlogic::CryptoProviders::Sha512 17 | end 18 | belongs_to :company 19 | has_and_belongs_to_many :projects 20 | 21 | # Validations 22 | # ----------- 23 | # 24 | # In Authlogic 4.4.0, we deprecated the features of Authlogic related to 25 | # validating email, login, and password. In 5.0.0 these features were dropped. 26 | # People will instead use normal ActiveRecord validations. 27 | # 28 | # The following validations represent what Authlogic < 5 used as defaults. 29 | validates :email, 30 | format: { 31 | with: EMAIL, 32 | message: proc { 33 | ::Authlogic::I18n.t( 34 | "error_messages.email_invalid", 35 | default: "should look like an email address." 36 | ) 37 | } 38 | }, 39 | length: { maximum: 100 }, 40 | uniqueness: { 41 | case_sensitive: false, 42 | if: :will_save_change_to_email? 43 | } 44 | 45 | validates :login, 46 | format: { 47 | with: LOGIN, 48 | message: proc { 49 | ::Authlogic::I18n.t( 50 | "error_messages.login_invalid", 51 | default: "should use only letters, numbers, spaces, and .-_@+ please." 52 | ) 53 | } 54 | }, 55 | length: { within: 3..100 }, 56 | uniqueness: { 57 | # Our User model will test `case_sensitive: true`. Other models, like 58 | # Employee and Admin do not validate uniqueness, and thus, for them, 59 | # `find_by_smart_case_login_field` will be case-insensitive. See eg. 60 | # `test_find_by_smart_case_login_field` in 61 | # `test/acts_as_authentic_test/login_test.rb` 62 | case_sensitive: true, 63 | if: :will_save_change_to_login? 64 | } 65 | 66 | validates :password, 67 | confirmation: { if: :require_password? }, 68 | length: { 69 | minimum: 8, 70 | if: :require_password? 71 | } 72 | validates :password_confirmation, 73 | length: { 74 | minimum: 8, 75 | if: :require_password? 76 | } 77 | end 78 | -------------------------------------------------------------------------------- /test/libs/user_session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UserSession < Authlogic::Session::Base 4 | end 5 | 6 | class BackOfficeUserSession < Authlogic::Session::Base 7 | end 8 | 9 | class WackyUserSession < Authlogic::Session::Base 10 | attr_accessor :counter 11 | authenticate_with User 12 | 13 | def initialize 14 | @counter = 0 15 | super 16 | end 17 | 18 | def persist_by_false 19 | self.counter += 1 20 | false 21 | end 22 | 23 | def persist_by_true 24 | self.counter += 1 25 | true 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/random_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RandomTest < ActiveSupport::TestCase 6 | def test_that_hex_tokens_are_unique 7 | tokens = Array.new(100) { Authlogic::Random.hex_token } 8 | assert_equal tokens.size, tokens.uniq.size 9 | end 10 | 11 | def test_that_friendly_tokens_are_unique 12 | tokens = Array.new(100) { Authlogic::Random.friendly_token } 13 | assert_equal tokens.size, tokens.uniq.size 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/session_test/activation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module ActivationTest 7 | class ClassMethodsTest < ActiveSupport::TestCase 8 | def test_activated 9 | assert UserSession.activated? 10 | Authlogic::Session::Base.controller = nil 11 | refute UserSession.activated? 12 | end 13 | 14 | def test_controller 15 | Authlogic::Session::Base.controller = nil 16 | assert_nil Authlogic::Session::Base.controller 17 | thread1 = Thread.new do 18 | controller = MockController.new 19 | Authlogic::Session::Base.controller = controller 20 | assert_equal controller, Authlogic::Session::Base.controller 21 | end 22 | thread1.join 23 | 24 | assert_nil Authlogic::Session::Base.controller 25 | 26 | thread2 = Thread.new do 27 | controller = MockController.new 28 | Authlogic::Session::Base.controller = controller 29 | assert_equal controller, Authlogic::Session::Base.controller 30 | end 31 | thread2.join 32 | 33 | assert_nil Authlogic::Session::Base.controller 34 | end 35 | end 36 | 37 | class InstanceMethodsTest < ActiveSupport::TestCase 38 | def test_init 39 | UserSession.controller = nil 40 | assert_raise(Authlogic::Session::Activation::NotActivatedError) { UserSession.new } 41 | UserSession.controller = controller 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/session_test/active_record_trickery_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module ActiveRecordTrickeryTest 7 | class ClassMethodsTest < ActiveSupport::TestCase 8 | # If test_human_name is executed after test_i18n_of_human_name the test will fail. 9 | i_suck_and_my_tests_are_order_dependent! 10 | 11 | def test_human_attribute_name 12 | assert_equal "Some attribute", UserSession.human_attribute_name("some_attribute") 13 | assert_equal "Some attribute", UserSession.human_attribute_name(:some_attribute) 14 | end 15 | 16 | def test_human_name 17 | assert_equal "Usersession", UserSession.human_name 18 | end 19 | 20 | def test_i18n_of_human_name 21 | I18n.backend.store_translations "en", authlogic: { models: { user_session: "MySession" } } 22 | assert_equal "MySession", UserSession.human_name 23 | end 24 | 25 | def test_i18n_of_model_name_human 26 | I18n.backend.store_translations "en", authlogic: { models: { user_session: "MySession" } } 27 | assert_equal "MySession", UserSession.model_name.human 28 | end 29 | 30 | def test_model_name 31 | assert_equal "UserSession", UserSession.model_name.name 32 | assert_equal "user_session", UserSession.model_name.singular 33 | assert_equal "user_sessions", UserSession.model_name.plural 34 | end 35 | end 36 | 37 | class InstanceMethodsTest < ActiveSupport::TestCase 38 | def test_new_record 39 | session = UserSession.new 40 | assert session.new_record? 41 | end 42 | 43 | def test_to_key 44 | ben = users(:ben) 45 | session = UserSession.new(ben) 46 | assert_nil session.to_key 47 | 48 | session.save 49 | assert_not_nil session.to_key 50 | assert_equal ben.to_key, session.to_key 51 | end 52 | 53 | def test_persisted 54 | session = UserSession.new(users(:ben)) 55 | refute session.persisted? 56 | 57 | session.save 58 | assert session.persisted? 59 | 60 | session.destroy 61 | refute session.persisted? 62 | end 63 | 64 | def test_destroyed? 65 | session = UserSession.create(users(:ben)) 66 | refute session.destroyed? 67 | 68 | session.destroy 69 | assert session.destroyed? 70 | end 71 | 72 | def test_to_model 73 | session = UserSession.new 74 | assert_equal session, session.to_model 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/session_test/brute_force_protection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module BruteForceProtectionTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_consecutive_failed_logins_limit 9 | UserSession.consecutive_failed_logins_limit = 10 10 | assert_equal 10, UserSession.consecutive_failed_logins_limit 11 | 12 | UserSession.consecutive_failed_logins_limit 50 13 | assert_equal 50, UserSession.consecutive_failed_logins_limit 14 | end 15 | 16 | def test_failed_login_ban_for 17 | UserSession.failed_login_ban_for = 10 18 | assert_equal 10, UserSession.failed_login_ban_for 19 | 20 | UserSession.failed_login_ban_for 2.hours 21 | assert_equal 2.hours.to_i, UserSession.failed_login_ban_for 22 | end 23 | end 24 | 25 | class InstanceMethodsTest < ActiveSupport::TestCase 26 | def test_under_limit 27 | ben = users(:ben) 28 | ben.failed_login_count = UserSession.consecutive_failed_logins_limit - 1 29 | assert ben.save 30 | session = UserSession.create(login: ben.login, password: "benrocks") 31 | refute session.new_session? 32 | end 33 | 34 | def test_exceeded_limit 35 | ben = users(:ben) 36 | ben.failed_login_count = UserSession.consecutive_failed_logins_limit 37 | assert ben.save 38 | session = UserSession.create(login: ben.login, password: "benrocks") 39 | assert session.new_session? 40 | assert UserSession.create(ben).new_session? 41 | ben.reload 42 | ben.updated_at = (UserSession.failed_login_ban_for + 2.hours.to_i).seconds.ago 43 | refute UserSession.create(ben).new_session? 44 | end 45 | 46 | def test_exceeding_failed_logins_limit 47 | UserSession.consecutive_failed_logins_limit = 2 48 | ben = users(:ben) 49 | 50 | 2.times do |i| 51 | session = UserSession.new(login: ben.login, password: "badpassword1") 52 | refute session.save 53 | refute session.errors[:password].empty? 54 | assert_equal i + 1, ben.reload.failed_login_count 55 | end 56 | 57 | session = UserSession.new(login: ben.login, password: "badpassword2") 58 | refute session.save 59 | assert session.errors[:password].empty? 60 | assert_equal 3, ben.reload.failed_login_count 61 | 62 | UserSession.consecutive_failed_logins_limit = 50 63 | end 64 | 65 | def test_exceeded_ban_for 66 | UserSession.consecutive_failed_logins_limit = 2 67 | UserSession.generalize_credentials_error_messages true 68 | ben = users(:ben) 69 | 70 | 2.times do |i| 71 | session = UserSession.new(login: ben.login, password: "badpassword1") 72 | refute session.save 73 | assert session.invalid_password? 74 | assert_equal i + 1, ben.reload.failed_login_count 75 | end 76 | 77 | ActiveRecord::Base.connection.execute( 78 | "update users set updated_at = '#{1.day.ago.to_formatted_s(:db)}' 79 | where login = '#{ben.login}'" 80 | ) 81 | session = UserSession.new(login: ben.login, password: "benrocks") 82 | assert session.save 83 | assert_equal 0, ben.reload.failed_login_count 84 | 85 | UserSession.consecutive_failed_logins_limit = 50 86 | UserSession.generalize_credentials_error_messages false 87 | end 88 | 89 | def test_exceeded_ban_and_failed_doesnt_ban_again 90 | UserSession.consecutive_failed_logins_limit = 2 91 | ben = users(:ben) 92 | 93 | 2.times do |i| 94 | session = UserSession.new(login: ben.login, password: "badpassword1") 95 | refute session.save 96 | refute session.errors[:password].empty? 97 | assert_equal i + 1, ben.reload.failed_login_count 98 | end 99 | 100 | ActiveRecord::Base.connection.execute( 101 | "update users set updated_at = '#{1.day.ago.to_formatted_s(:db)}' 102 | where login = '#{ben.login}'" 103 | ) 104 | session = UserSession.new(login: ben.login, password: "badpassword1") 105 | refute session.save 106 | assert_equal 1, ben.reload.failed_login_count 107 | 108 | UserSession.consecutive_failed_logins_limit = 50 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /test/session_test/callbacks_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class CallbacksTest < ActiveSupport::TestCase 7 | def setup 8 | WackyUserSession.reset_callbacks(:persist) 9 | end 10 | 11 | def test_no_callbacks 12 | assert_equal [], WackyUserSession._persist_callbacks.map(&:filter) 13 | session = WackyUserSession.new 14 | session.send(:run_callbacks, :persist) 15 | assert_equal 0, session.counter 16 | end 17 | 18 | def test_true_callback_cancelling_later_callbacks 19 | WackyUserSession.persist :persist_by_true, :persist_by_false 20 | assert_equal( 21 | %i[persist_by_true persist_by_false], 22 | WackyUserSession._persist_callbacks.map(&:filter) 23 | ) 24 | 25 | session = WackyUserSession.new 26 | session.send(:run_callbacks, :persist) 27 | assert_equal 1, session.counter 28 | end 29 | 30 | def test_false_callback_continuing_to_later_callbacks 31 | WackyUserSession.persist :persist_by_false, :persist_by_true 32 | assert_equal( 33 | %i[persist_by_false persist_by_true], 34 | WackyUserSession._persist_callbacks.map(&:filter) 35 | ) 36 | 37 | session = WackyUserSession.new 38 | session.send(:run_callbacks, :persist) 39 | assert_equal 2, session.counter 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/session_test/credentials_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/binarylogic/authlogic/f2c01d453ec7f4ded078483e010f1673dc13d3c6/test/session_test/credentials_test.rb -------------------------------------------------------------------------------- /test/session_test/existence_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module ExistenceTest 7 | class ClassMethodsTest < ActiveSupport::TestCase 8 | def test_create_with_good_credentials 9 | ben = users(:ben) 10 | session = UserSession.create(login: ben.login, password: "benrocks") 11 | refute session.new_session? 12 | end 13 | 14 | def test_create_with_bad_credentials 15 | session = UserSession.create(login: "somelogin", password: "badpw2") 16 | assert session.new_session? 17 | end 18 | 19 | def test_create_bang 20 | ben = users(:ben) 21 | err = assert_raise(Authlogic::Session::Existence::SessionInvalidError) do 22 | UserSession.create!(login: ben.login, password: "badpw") 23 | end 24 | assert_includes err.message, "Password is not valid" 25 | refute UserSession.create!(login: ben.login, password: "benrocks").new_session? 26 | end 27 | end 28 | 29 | class InstanceMethodsTest < ActiveSupport::TestCase 30 | def test_new_session 31 | session = UserSession.new 32 | assert session.new_session? 33 | 34 | set_session_for(users(:ben)) 35 | session = UserSession.find 36 | refute session.new_session? 37 | end 38 | 39 | def test_save_with_nothing 40 | session = UserSession.new 41 | refute session.save 42 | assert session.new_session? 43 | end 44 | 45 | def test_save_with_block 46 | session = UserSession.new 47 | block_result = session.save do |result| 48 | refute result 49 | end 50 | refute block_result 51 | assert session.new_session? 52 | end 53 | 54 | def test_save_with_bang 55 | session = UserSession.new 56 | assert_raise(Authlogic::Session::Existence::SessionInvalidError) { session.save! } 57 | 58 | session.unauthorized_record = users(:ben) 59 | assert_nothing_raised { session.save! } 60 | end 61 | 62 | def test_destroy 63 | ben = users(:ben) 64 | session = UserSession.new 65 | refute session.valid? 66 | refute session.errors.empty? 67 | assert session.destroy 68 | assert session.errors.empty? 69 | session.unauthorized_record = ben 70 | assert session.save 71 | assert session.record 72 | assert session.destroy 73 | refute session.record 74 | end 75 | end 76 | 77 | class SessionInvalidErrorTest < ActiveSupport::TestCase 78 | def test_message 79 | session = UserSession.new 80 | assert !session.valid? 81 | error = Authlogic::Session::Existence::SessionInvalidError.new(session) 82 | message = "Your session is invalid and has the following errors: " + 83 | session.errors.full_messages.to_sentence 84 | assert_equal message, error.message 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/session_test/foundation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | # We forbid the use of AC::Parameters, and we have a test to that effect, but we 6 | # do not want a development dependency on `actionpack`, so we define it here. 7 | module ActionController 8 | class Parameters; end 9 | end 10 | 11 | module SessionTest 12 | class FoundationTest < ActiveSupport::TestCase 13 | def test_credentials_raise_if_not_a_hash 14 | session = UserSession.new 15 | e = assert_raises(TypeError) { 16 | session.credentials = ActionController::Parameters.new 17 | } 18 | assert_equal( 19 | ::Authlogic::Session::Base::E_AC_PARAMETERS, 20 | e.message 21 | ) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/session_test/http_auth_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class HttpAuthTest < ActiveSupport::TestCase 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_allow_http_basic_auth 9 | UserSession.allow_http_basic_auth = false 10 | assert_equal false, UserSession.allow_http_basic_auth 11 | 12 | UserSession.allow_http_basic_auth true 13 | assert_equal true, UserSession.allow_http_basic_auth 14 | end 15 | 16 | def test_request_http_basic_auth 17 | UserSession.request_http_basic_auth = true 18 | assert_equal true, UserSession.request_http_basic_auth 19 | 20 | UserSession.request_http_basic_auth = false 21 | assert_equal false, UserSession.request_http_basic_auth 22 | end 23 | 24 | def test_http_basic_auth_realm 25 | assert_equal "Application", UserSession.http_basic_auth_realm 26 | UserSession.http_basic_auth_realm = "TestRealm" 27 | assert_equal "TestRealm", UserSession.http_basic_auth_realm 28 | end 29 | end 30 | 31 | class InstanceMethodsTest < ActiveSupport::TestCase 32 | def test_persist_persist_by_http_auth 33 | UserSession.allow_http_basic_auth = true 34 | 35 | aaron = users(:aaron) 36 | http_basic_auth_for do 37 | refute UserSession.find 38 | end 39 | http_basic_auth_for(aaron) do 40 | assert session = UserSession.find 41 | assert_equal aaron, session.record 42 | assert_equal aaron.login, session.login 43 | assert_equal "aaronrocks", session.send(:protected_password) 44 | refute controller.http_auth_requested? 45 | end 46 | unset_session 47 | UserSession.request_http_basic_auth = true 48 | UserSession.http_basic_auth_realm = "PersistTestRealm" 49 | http_basic_auth_for(aaron) do 50 | assert session = UserSession.find 51 | assert_equal aaron, session.record 52 | assert_equal aaron.login, session.login 53 | assert_equal "aaronrocks", session.send(:protected_password) 54 | assert_equal "PersistTestRealm", controller.realm 55 | assert controller.http_auth_requested? 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/session_test/id_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class IdTest < ActiveSupport::TestCase 7 | def test_credentials 8 | session = UserSession.new 9 | session.credentials = [:my_id] 10 | assert_equal :my_id, session.id 11 | end 12 | 13 | def test_id 14 | session = UserSession.new 15 | session.id = :my_id 16 | assert_equal :my_id, session.id 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/session_test/klass_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module KlassTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_authenticate_with 9 | UserSession.authenticate_with = Employee 10 | assert_equal "Employee", UserSession.klass_name 11 | assert_equal Employee, UserSession.klass 12 | 13 | UserSession.authenticate_with User 14 | assert_equal "User", UserSession.klass_name 15 | assert_equal User, UserSession.klass 16 | end 17 | 18 | def test_klass 19 | assert_equal User, UserSession.klass 20 | end 21 | 22 | def test_klass_name 23 | assert_equal "User", UserSession.klass_name 24 | end 25 | 26 | def test_klass_name_uses_custom_name 27 | assert_equal "User", UserSession.klass_name 28 | assert_equal "BackOfficeUser", BackOfficeUserSession.klass_name 29 | end 30 | end 31 | 32 | class InstanceMethodsTest < ActiveSupport::TestCase 33 | def test_record_method 34 | ben = users(:ben) 35 | set_session_for(ben) 36 | session = UserSession.find 37 | assert_equal ben, session.record 38 | assert_equal ben, session.user 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/session_test/magic_columns_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module MagicColumnsTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_last_request_at_threshold_config 9 | UserSession.last_request_at_threshold = 2.minutes 10 | assert_equal 2.minutes, UserSession.last_request_at_threshold 11 | 12 | UserSession.last_request_at_threshold 0 13 | assert_equal 0, UserSession.last_request_at_threshold 14 | end 15 | end 16 | 17 | class InstanceMethodsTest < ActiveSupport::TestCase 18 | def test_after_persisting_set_last_request_at 19 | ben = users(:ben) 20 | refute UserSession.create(ben).new_session? 21 | 22 | set_cookie_for(ben) 23 | old_last_request_at = ben.last_request_at 24 | assert UserSession.find 25 | ben.reload 26 | assert ben.last_request_at > old_last_request_at 27 | end 28 | 29 | def test_valid_increase_failed_login_count 30 | ben = users(:ben) 31 | old_failed_login_count = ben.failed_login_count 32 | session = UserSession.create(login: ben.login, password: "wrong") 33 | assert session.new_session? 34 | ben.reload 35 | assert_equal old_failed_login_count + 1, ben.failed_login_count 36 | end 37 | 38 | def test_before_save_update_info 39 | aaron = users(:aaron) 40 | 41 | # increase failed login count 42 | session = UserSession.create(login: aaron.login, password: "wrong") 43 | assert session.new_session? 44 | aaron.reload 45 | assert_equal 0, aaron.login_count 46 | assert_nil aaron.current_login_at 47 | assert_nil aaron.current_login_ip 48 | 49 | session = UserSession.create(login: aaron.login, password: "aaronrocks") 50 | assert session.valid? 51 | 52 | aaron.reload 53 | assert_equal 1, aaron.login_count 54 | assert_equal 0, aaron.failed_login_count 55 | assert_nil aaron.last_login_at 56 | assert_not_nil aaron.current_login_at 57 | assert_nil aaron.last_login_ip 58 | assert_equal "1.1.1.1", aaron.current_login_ip 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/session_test/magic_states_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module SessionTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_disable_magic_states_config 9 | UserSession.disable_magic_states = true 10 | assert_equal true, UserSession.disable_magic_states 11 | 12 | UserSession.disable_magic_states false 13 | assert_equal false, UserSession.disable_magic_states 14 | end 15 | end 16 | 17 | class InstanceMethodsTest < ActiveSupport::TestCase 18 | def test_disabling_magic_states 19 | UserSession.disable_magic_states = true 20 | ben = users(:ben) 21 | ben.update_attribute(:active, false) 22 | refute UserSession.create(ben).new_session? 23 | UserSession.disable_magic_states = false 24 | end 25 | 26 | def test_validate_validate_magic_states_active 27 | session = UserSession.new 28 | ben = users(:ben) 29 | session.unauthorized_record = ben 30 | assert session.valid? 31 | 32 | ben.update_attribute(:active, false) 33 | refute session.valid? 34 | refute session.errors[:base].empty? 35 | end 36 | 37 | def test_validate_validate_magic_states_approved 38 | session = UserSession.new 39 | ben = users(:ben) 40 | session.unauthorized_record = ben 41 | assert session.valid? 42 | 43 | ben.update_attribute(:approved, false) 44 | refute session.valid? 45 | refute session.errors[:base].empty? 46 | end 47 | 48 | def test_validate_validate_magic_states_confirmed 49 | session = UserSession.new 50 | ben = users(:ben) 51 | session.unauthorized_record = ben 52 | assert session.valid? 53 | 54 | ben.update_attribute(:confirmed, false) 55 | refute session.valid? 56 | refute session.errors[:base].empty? 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/session_test/params_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module ParamsTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_params_key 9 | UserSession.params_key = "my_params_key" 10 | assert_equal "my_params_key", UserSession.params_key 11 | 12 | UserSession.params_key "user_credentials" 13 | assert_equal "user_credentials", UserSession.params_key 14 | end 15 | 16 | def test_single_access_allowed_request_types 17 | UserSession.single_access_allowed_request_types = ["my request type"] 18 | assert_equal ["my request type"], UserSession.single_access_allowed_request_types 19 | UserSession.single_access_allowed_request_types( 20 | ["application/rss+xml", "application/atom+xml"] 21 | ) 22 | assert_equal( 23 | ["application/rss+xml", "application/atom+xml"], 24 | UserSession.single_access_allowed_request_types 25 | ) 26 | end 27 | end 28 | 29 | class InstanceMethodsTest < ActiveSupport::TestCase 30 | def test_persist_persist_by_params 31 | ben = users(:ben) 32 | session = UserSession.new 33 | 34 | refute session.persisting? 35 | set_params_for(ben) 36 | 37 | refute session.persisting? 38 | refute session.unauthorized_record 39 | refute session.record 40 | assert_nil controller.session["user_credentials"] 41 | 42 | set_request_content_type("text/plain") 43 | refute session.persisting? 44 | refute session.unauthorized_record 45 | assert_nil controller.session["user_credentials"] 46 | 47 | set_request_content_type("application/atom+xml") 48 | assert session.persisting? 49 | assert_equal ben, session.record 50 | 51 | # should not persist since this is single access 52 | assert_nil controller.session["user_credentials"] 53 | 54 | set_request_content_type("application/rss+xml") 55 | assert session.persisting? 56 | assert_equal ben, session.unauthorized_record 57 | assert_nil controller.session["user_credentials"] 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/session_test/password_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module PasswordTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_find_by_login_method_is_deprecated 9 | expected_warning = Regexp.new( 10 | Regexp.escape(::Authlogic::Session::Base::E_DPR_FIND_BY_LOGIN_METHOD) 11 | ) 12 | 13 | assert_output(nil, expected_warning) do 14 | UserSession.find_by_login_method = "my_login_method" 15 | end 16 | assert_equal "my_login_method", UserSession.record_selection_method 17 | 18 | assert_output(nil, expected_warning) do 19 | UserSession.find_by_login_method "find_by_login" 20 | end 21 | assert_equal "find_by_login", UserSession.record_selection_method 22 | end 23 | 24 | def test_record_selection_method 25 | UserSession.record_selection_method = "my_login_method" 26 | assert_equal "my_login_method", UserSession.record_selection_method 27 | 28 | UserSession.record_selection_method "find_by_login" 29 | assert_equal "find_by_login", UserSession.record_selection_method 30 | end 31 | 32 | def test_verify_password_method 33 | UserSession.verify_password_method = "my_login_method" 34 | assert_equal "my_login_method", UserSession.verify_password_method 35 | 36 | UserSession.verify_password_method "valid_password?" 37 | assert_equal "valid_password?", UserSession.verify_password_method 38 | end 39 | 40 | def test_generalize_credentials_error_mesages_set_to_false 41 | UserSession.generalize_credentials_error_messages false 42 | refute UserSession.generalize_credentials_error_messages 43 | session = UserSession.create(login: users(:ben).login, password: "invalud-password") 44 | assert_equal ["Password is not valid"], session.errors.full_messages 45 | end 46 | 47 | def test_generalize_credentials_error_messages_set_to_true 48 | UserSession.generalize_credentials_error_messages true 49 | assert UserSession.generalize_credentials_error_messages 50 | session = UserSession.create(login: users(:ben).login, password: "invalud-password") 51 | assert_equal ["Login/Password combination is not valid"], session.errors.full_messages 52 | end 53 | 54 | def test_generalize_credentials_error_messages_set_to_string 55 | UserSession.generalize_credentials_error_messages = "Custom Error Message" 56 | assert UserSession.generalize_credentials_error_messages 57 | session = UserSession.create(login: users(:ben).login, password: "invalud-password") 58 | assert_equal ["Custom Error Message"], session.errors.full_messages 59 | end 60 | 61 | def test_login_field 62 | UserSession.configured_password_methods = false 63 | UserSession.login_field = :saweet 64 | assert_equal :saweet, UserSession.login_field 65 | session = UserSession.new 66 | assert session.respond_to?(:saweet) 67 | 68 | UserSession.login_field :login 69 | assert_equal :login, UserSession.login_field 70 | session = UserSession.new 71 | assert session.respond_to?(:login) 72 | end 73 | 74 | def test_password_field 75 | UserSession.configured_password_methods = false 76 | UserSession.password_field = :saweet 77 | assert_equal :saweet, UserSession.password_field 78 | session = UserSession.new 79 | assert session.respond_to?(:saweet) 80 | 81 | UserSession.password_field :password 82 | assert_equal :password, UserSession.password_field 83 | session = UserSession.new 84 | assert session.respond_to?(:password) 85 | end 86 | end 87 | 88 | class InstanceMethodsTest < ActiveSupport::TestCase 89 | def test_init 90 | session = UserSession.new 91 | assert session.respond_to?(:login) 92 | assert session.respond_to?(:login=) 93 | assert session.respond_to?(:password) 94 | assert session.respond_to?(:password=) 95 | assert session.respond_to?(:protected_password, true) 96 | end 97 | 98 | def test_credentials 99 | session = UserSession.new 100 | session.credentials = { login: "login", password: "pass" } 101 | assert_equal "login", session.login 102 | assert_nil session.password 103 | assert_equal "pass", session.send(:protected_password) 104 | assert_equal({ password: "", login: "login" }, session.credentials) 105 | end 106 | 107 | def test_credentials_are_params_safe 108 | session = UserSession.new 109 | assert_nothing_raised { session.credentials = { hacker_method: "error!" } } 110 | end 111 | 112 | def test_save_with_credentials 113 | aaron = users(:aaron) 114 | session = UserSession.new(login: aaron.login, password: "aaronrocks") 115 | assert session.save 116 | refute session.new_session? 117 | assert_equal 1, session.record.login_count 118 | assert Time.now >= session.record.current_login_at 119 | assert_equal "1.1.1.1", session.record.current_login_ip 120 | assert_equal env_session_options[:renew], true 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /test/session_test/perishability_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class PerishabilityTest < ActiveSupport::TestCase 7 | def test_after_save 8 | ben = users(:ben) 9 | old_perishable_token = ben.perishable_token 10 | UserSession.create(ben) 11 | assert_not_equal old_perishable_token, ben.perishable_token 12 | 13 | drew = employees(:drew) 14 | refute UserSession.create(drew).new_session? 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/session_test/persistence_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "authlogic/controller_adapters/rails_adapter" 5 | 6 | module SessionTest 7 | class PersistenceTest < ActiveSupport::TestCase 8 | def test_find 9 | aaron = users(:aaron) 10 | refute UserSession.find 11 | UserSession.allow_http_basic_auth = true 12 | http_basic_auth_for(aaron) { assert UserSession.find } 13 | set_cookie_for(aaron) 14 | assert UserSession.find 15 | unset_cookie 16 | set_session_for(aaron) 17 | session = UserSession.find 18 | assert session 19 | end 20 | 21 | def test_find_in_api 22 | @controller = Authlogic::TestCase::MockAPIController.new 23 | UserSession.controller = 24 | Authlogic::ControllerAdapters::RailsAdapter.new(@controller) 25 | 26 | aaron = users(:aaron) 27 | refute UserSession.find 28 | 29 | UserSession.single_access_allowed_request_types = ["application/json"] 30 | set_params_for(aaron) 31 | set_request_content_type("application/json") 32 | assert UserSession.find 33 | end 34 | 35 | def test_persisting 36 | # tested thoroughly in test_find 37 | end 38 | 39 | def test_should_set_remember_me_on_the_next_request 40 | aaron = users(:aaron) 41 | session = UserSession.new(aaron) 42 | session.remember_me = true 43 | refute UserSession.remember_me 44 | assert session.save 45 | assert session.remember_me? 46 | session = UserSession.find(aaron) 47 | assert session.remember_me? 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/session_test/scopes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class ScopesTest < ActiveSupport::TestCase 7 | def test_scope_method 8 | assert_nil Authlogic::Session::Base.scope 9 | 10 | thread1 = Thread.new do 11 | scope = { id: :scope1 } 12 | Authlogic::Session::Base.send(:scope=, scope) 13 | assert_equal scope, Authlogic::Session::Base.scope 14 | end 15 | thread1.join 16 | 17 | assert_nil Authlogic::Session::Base.scope 18 | 19 | thread2 = Thread.new do 20 | scope = { id: :scope2 } 21 | Authlogic::Session::Base.send(:scope=, scope) 22 | assert_equal scope, Authlogic::Session::Base.scope 23 | end 24 | thread2.join 25 | 26 | assert_nil Authlogic::Session::Base.scope 27 | end 28 | 29 | def test_with_scope_method 30 | assert_raise(ArgumentError) { UserSession.with_scope } 31 | 32 | UserSession.with_scope(find_options: { conditions: "awesome = 1" }, id: "some_id") do 33 | assert_equal( 34 | { find_options: { conditions: "awesome = 1" }, id: "some_id" }, 35 | UserSession.scope 36 | ) 37 | end 38 | 39 | assert_nil UserSession.scope 40 | end 41 | 42 | def test_initialize 43 | UserSession.with_scope(find_options: { conditions: "awesome = 1" }, id: "some_id") do 44 | session = UserSession.new 45 | assert_equal( 46 | { find_options: { conditions: "awesome = 1" }, id: "some_id" }, 47 | session.scope 48 | ) 49 | session.id = :another_id 50 | assert_equal "another_id_some_id_test", session.send(:build_key, "test") 51 | end 52 | end 53 | 54 | def test_search_for_record_with_scopes 55 | binary_logic = companies(:binary_logic) 56 | ben = users(:ben) 57 | zack = users(:zack) 58 | 59 | session = UserSession.new 60 | assert_equal zack, session.send(:search_for_record, "find_by_login", zack.login) 61 | 62 | session.scope = { find_options: { conditions: ["company_id = ?", binary_logic.id] } } 63 | assert_nil session.send(:search_for_record, "find_by_login", zack.login) 64 | 65 | assert_equal ben, session.send(:search_for_record, "find_by_login", ben.login) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/session_test/session_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module SessionTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_session_key 9 | UserSession.session_key = "my_session_key" 10 | assert_equal "my_session_key", UserSession.session_key 11 | 12 | UserSession.session_key "user_credentials" 13 | assert_equal "user_credentials", UserSession.session_key 14 | end 15 | end 16 | 17 | class InstanceMethodsTest < ActiveSupport::TestCase 18 | def test_persist_persist_by_session 19 | ben = users(:ben) 20 | set_session_for(ben) 21 | assert session = UserSession.find 22 | assert_equal ben, session.record 23 | assert_equal ben.persistence_token, controller.session["user_credentials"] 24 | refute_includes env_session_options, :renew 25 | end 26 | 27 | # A SQL injection attack to steal the persistence_token. 28 | # TODO: Explain how `:select` is used, and sanitized. 29 | def test_persist_persist_by_session_with_sql_injection_attack 30 | ben = users(:ben) 31 | controller.session["user_credentials"] = "neo" 32 | controller.session["user_credentials_id"] = { 33 | select: " *,'neo' AS persistence_token FROM users WHERE id = #{ben.id} limit 1 -- " 34 | } 35 | @user_session = UserSession.find 36 | assert @user_session.blank? 37 | end 38 | 39 | def test_persist_persist_by_session_with_sql_injection_attack_2 40 | controller.session["user_credentials"] = { select: "ABRA CADABRA" } 41 | controller.session["user_credentials_id"] = nil 42 | assert_nothing_raised do 43 | @user_session = UserSession.find 44 | end 45 | assert @user_session.blank? 46 | end 47 | 48 | def test_persist_persist_by_session_with_token_only 49 | ben = users(:ben) 50 | set_session_for(ben) 51 | controller.session["user_credentials_id"] = nil 52 | session = UserSession.find 53 | assert_equal ben, session.record 54 | assert_equal ben.persistence_token, controller.session["user_credentials"] 55 | refute_includes env_session_options, :renew 56 | end 57 | 58 | def test_after_save_update_session 59 | ben = users(:ben) 60 | session = UserSession.new(ben) 61 | assert controller.session["user_credentials"].blank? 62 | assert session.save 63 | assert_equal ben.persistence_token, controller.session["user_credentials"] 64 | assert_equal env_session_options[:renew], true 65 | end 66 | 67 | def test_after_destroy_update_session 68 | ben = users(:ben) 69 | set_session_for(ben) 70 | assert_equal ben.persistence_token, controller.session["user_credentials"] 71 | assert session = UserSession.find 72 | assert session.destroy 73 | assert controller.session["user_credentials"].blank? 74 | refute_includes env_session_options, :renew 75 | end 76 | 77 | def test_after_persisting_update_session 78 | ben = users(:ben) 79 | set_cookie_for(ben) 80 | assert controller.session["user_credentials"].blank? 81 | assert UserSession.find 82 | assert_equal ben.persistence_token, controller.session["user_credentials"] 83 | refute_includes env_session_options, :renew 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/session_test/timeout_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | module TimeoutTest 7 | class ConfigTest < ActiveSupport::TestCase 8 | def test_logout_on_timeout 9 | UserSession.logout_on_timeout = true 10 | assert UserSession.logout_on_timeout 11 | 12 | UserSession.logout_on_timeout false 13 | refute UserSession.logout_on_timeout 14 | end 15 | end 16 | 17 | class InstanceMethods < ActiveSupport::TestCase 18 | def test_stale_state 19 | UserSession.logout_on_timeout = true 20 | ben = users(:ben) 21 | ben.last_request_at = 3.years.ago 22 | ben.save 23 | set_session_for(ben) 24 | 25 | session = UserSession.new 26 | assert session.persisting? 27 | assert session.stale? 28 | assert_equal ben, session.stale_record 29 | assert_nil session.record 30 | assert_nil controller.session["user_credentials_id"] 31 | 32 | set_session_for(ben) 33 | 34 | ben.last_request_at = Time.now 35 | ben.save 36 | 37 | assert session.persisting? 38 | refute session.stale? 39 | assert_nil session.stale_record 40 | 41 | UserSession.logout_on_timeout = false 42 | end 43 | 44 | def test_should_be_stale_with_expired_remember_date 45 | UserSession.logout_on_timeout = true 46 | UserSession.remember_me = true 47 | UserSession.remember_me_for = 3.months 48 | ben = users(:ben) 49 | assert ben.save 50 | session = UserSession.new(ben) 51 | assert session.save 52 | Timecop.freeze(Time.now + 4.month) 53 | assert session.persisting? 54 | assert session.stale? 55 | UserSession.remember_me = false 56 | end 57 | 58 | def test_should_not_be_stale_with_valid_remember_date 59 | UserSession.logout_on_timeout = true # Default is 10.minutes 60 | UserSession.remember_me = true 61 | UserSession.remember_me_for = 3.months 62 | ben = users(:ben) 63 | assert ben.save 64 | session = UserSession.new(ben) 65 | assert session.save 66 | Timecop.freeze(Time.now + 2.months) 67 | assert session.persisting? 68 | refute session.stale? 69 | UserSession.remember_me = false 70 | end 71 | 72 | def test_successful_login 73 | UserSession.logout_on_timeout = true 74 | ben = users(:ben) 75 | session = UserSession.create(login: ben.login, password: "benrocks") 76 | refute session.new_session? 77 | session = UserSession.find 78 | assert session 79 | assert_equal ben, session.record 80 | UserSession.logout_on_timeout = false 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/session_test/unauthorized_record_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class UnauthorizedRecordTest < ActiveSupport::TestCase 7 | def test_credentials 8 | ben = users(:ben) 9 | session = UserSession.new 10 | session.credentials = [ben] 11 | assert_equal ben, session.unauthorized_record 12 | assert_equal({ unauthorized_record: "" }, session.credentials) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/session_test/validation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | module SessionTest 6 | class ValidationTest < ActiveSupport::TestCase 7 | def test_errors 8 | session = UserSession.new 9 | assert_kind_of ::ActiveModel::Errors, session.errors 10 | end 11 | 12 | def test_valid 13 | session = UserSession.new 14 | refute session.valid? 15 | assert_nil session.record 16 | assert session.errors.count > 0 17 | 18 | ben = users(:ben) 19 | session.unauthorized_record = ben 20 | assert session.valid? 21 | assert_equal ben, session.attempted_record 22 | assert session.errors.empty? 23 | end 24 | end 25 | end 26 | --------------------------------------------------------------------------------